diff --git a/app/Console/Commands/InviteAddTemporary.php b/app/Console/Commands/InviteAddTemporary.php new file mode 100644 index 00000000..3956be1e --- /dev/null +++ b/app/Console/Commands/InviteAddTemporary.php @@ -0,0 +1,39 @@ +argument('uid'); + $days = $this->argument('days'); + $count = $this->argument('count'); + $this->info("uid: $uid, days: $days, count: $count"); + $uidArr = preg_split('/[\s,]+/', $uid); + GenerateTemporaryInvite::dispatch($uidArr, $days, $count); + return Command::SUCCESS; + } +} diff --git a/app/Console/Commands/Test.php b/app/Console/Commands/Test.php index 42e90efa..b80b23ac 100644 --- a/app/Console/Commands/Test.php +++ b/app/Console/Commands/Test.php @@ -12,6 +12,7 @@ use App\Models\Exam; use App\Models\ExamProgress; use App\Models\ExamUser; use App\Models\HitAndRun; +use App\Models\Invite; use App\Models\Medal; use App\Models\Peer; use App\Models\SearchBox; @@ -94,10 +95,9 @@ class Test extends Command */ public function handle() { - $rep = new WorkRepository(); - $rep->settleRole(4); + $uid = "2,3,4"; + $uidArr = preg_split('/[\s,]+/', $uid); + dd($uidArr); } - - } diff --git a/app/Filament/Resources/User/InviteResource.php b/app/Filament/Resources/User/InviteResource.php new file mode 100644 index 00000000..e45f28e1 --- /dev/null +++ b/app/Filament/Resources/User/InviteResource.php @@ -0,0 +1,112 @@ +schema([ + // + ]); + } + + public static function table(Table $table): Table + { + return $table + ->columns([ + Tables\Columns\TextColumn::make('id'), + Tables\Columns\TextColumn::make('inviter') + ->label(__('invite.fields.inviter')) + ->formatStateUsing(fn ($state) => username_for_admin($state)) + , + Tables\Columns\TextColumn::make('invitee') + ->label(__('invite.fields.invitee')) + , + Tables\Columns\TextColumn::make('hash') + , + Tables\Columns\TextColumn::make('time_invited') + ->label(__('invite.fields.time_invited')) + , + Tables\Columns\IconColumn::make('valid') + ->label(__('invite.fields.valid')) + ->boolean() + , + Tables\Columns\TextColumn::make('invitee_register_uid') + ->label(__('invite.fields.invitee_register_uid')) + , + Tables\Columns\TextColumn::make('invitee_register_email') + ->label(__('invite.fields.invitee_register_email')) + , + Tables\Columns\TextColumn::make('invitee_register_email') + ->label(__('invite.fields.invitee_register_email')) + , + Tables\Columns\TextColumn::make('invitee_register_username') + ->label(__('invite.fields.invitee_register_username')) + , + Tables\Columns\TextColumn::make('expired_at') + ->label(__('invite.fields.expired_at')) + ->formatStateUsing(fn ($state) => format_datetime($state)) + , + Tables\Columns\TextColumn::make('created_at') + ->label(__('label.created_at')) + ->formatStateUsing(fn ($state) => format_datetime($state)) + , + ]) + ->filters([ + // + ]) + ->actions([ +// Tables\Actions\EditAction::make(), + ]) + ->bulkActions([ +// Tables\Actions\DeleteBulkAction::make(), + ]); + } + + public static function getRelations(): array + { + return [ + // + ]; + } + + public static function getPages(): array + { + return [ + 'index' => Pages\ListInvites::route('/'), + 'create' => Pages\CreateInvite::route('/create'), + 'edit' => Pages\EditInvite::route('/{record}/edit'), + ]; + } +} diff --git a/app/Filament/Resources/User/InviteResource/Pages/CreateInvite.php b/app/Filament/Resources/User/InviteResource/Pages/CreateInvite.php new file mode 100644 index 00000000..a46184df --- /dev/null +++ b/app/Filament/Resources/User/InviteResource/Pages/CreateInvite.php @@ -0,0 +1,12 @@ +with(['inviter_user']); + } +} diff --git a/app/Filament/Resources/User/UserResource/Pages/UserProfile.php b/app/Filament/Resources/User/UserResource/Pages/UserProfile.php index 5a45a121..c1f89c58 100644 --- a/app/Filament/Resources/User/UserResource/Pages/UserProfile.php +++ b/app/Filament/Resources/User/UserResource/Pages/UserProfile.php @@ -3,12 +3,14 @@ namespace App\Filament\Resources\User\UserResource\Pages; use App\Filament\Resources\User\UserResource; +use App\Models\Invite; use App\Models\Medal; use App\Models\User; use App\Models\UserMeta; use App\Repositories\ExamRepository; use App\Repositories\MedalRepository; use App\Repositories\UserRepository; +use Carbon\Carbon; use Filament\Resources\Pages\Concerns\HasRelationManagers; use Filament\Resources\Pages\Concerns\InteractsWithRecord; use Filament\Resources\Pages\Page; @@ -129,20 +131,44 @@ class UserProfile extends ViewRecord 'invites' => __('label.user.invites'), 'seedbonus' => __('label.user.seedbonus'), 'attendance_card' => __('label.user.attendance_card'), - ])->label(__('admin.resources.user.actions.change_bonus_etc_field_label'))->inline()->required(), + 'tmp_invites' => __('label.user.tmp_invites'), + ]) + ->label(__('admin.resources.user.actions.change_bonus_etc_field_label')) + ->inline() + ->required() + ->reactive() + , Forms\Components\Radio::make('action')->options([ 'Increment' => __("admin.resources.user.actions.change_bonus_etc_action_increment"), 'Decrement' => __("admin.resources.user.actions.change_bonus_etc_action_decrement"), - ])->label(__('admin.resources.user.actions.change_bonus_etc_action_label'))->inline()->required(), + ]) + ->label(__('admin.resources.user.actions.change_bonus_etc_action_label')) + ->inline() + ->required() + , Forms\Components\TextInput::make('value')->integer()->required() ->label(__('admin.resources.user.actions.change_bonus_etc_value_label')) - ->helperText(__('admin.resources.user.actions.change_bonus_etc_value_help')), - Forms\Components\Textarea::make('reason')->label(__('admin.resources.user.actions.change_bonus_etc_reason_label')), + ->helperText(__('admin.resources.user.actions.change_bonus_etc_value_help')) + , + + Forms\Components\TextInput::make('duration')->integer() + ->label(__('admin.resources.user.actions.change_bonus_etc_duration_label')) + ->helperText(__('admin.resources.user.actions.change_bonus_etc_duration_help')) + ->hidden(fn (\Closure $get) => $get('field') != 'tmp_invites') + , + + Forms\Components\Textarea::make('reason') + ->label(__('admin.resources.user.actions.change_bonus_etc_reason_label')) + , ]) ->action(function ($data) { $userRep = $this->getRep(); try { - $userRep->incrementDecrement(Auth::user(), $this->record->id, $data['action'], $data['field'], $data['value'], $data['reason']); + if ($data['field'] == 'tmp_invites') { + $userRep->addTemporaryInvite(Auth::user(), $this->record->id, $data['action'], $data['value'], $data['duration'], $data['reason']); + } else { + $userRep->incrementDecrement(Auth::user(), $this->record->id, $data['action'], $data['field'], $data['value'], $data['reason']); + } $this->notify('success', 'Success!'); $this->emitSelf(self::EVENT_RECORD_UPDATED, $this->record->id); } catch (\Exception $exception) { @@ -298,6 +324,7 @@ class UserProfile extends ViewRecord { return [ 'props' => $this->listUserProps(), + 'temporary_invite_count' => $this->countTemporaryInvite() ]; } @@ -319,4 +346,13 @@ class UserProfile extends ViewRecord } return $props; } + + private function countTemporaryInvite() + { + return Invite::query()->where('inviter', $this->record->id) + ->where('invitee', '') + ->whereNotNull('expired_at') + ->where('expired_at', '>', Carbon::now()) + ->count(); + } } diff --git a/app/Jobs/GenerateTemporaryInvite.php b/app/Jobs/GenerateTemporaryInvite.php new file mode 100644 index 00000000..234cc24e --- /dev/null +++ b/app/Jobs/GenerateTemporaryInvite.php @@ -0,0 +1,81 @@ +uidArr = $uidArr; + $this->days = $days; + $this->count = $count; + } + + /** + * Determine the time at which the job should timeout. + * + * @return \DateTime + */ + public function retryUntil() + { + return now()->addHours(1); + } + + /** + * Execute the job. + * + * @return void + */ + public function handle() + { + $toolRep = new ToolRepository(); + foreach ($this->uidArr as $uid) { + try { + $hashArr = $toolRep->generateUniqueInviteHash([], $this->count, $this->count); + $data = []; + foreach($hashArr as $hash) { + $data[] = [ + 'inviter' => $uid, + 'invitee' => '', + 'hash' => $hash, + 'valid' => 0, + 'expired_at' => Carbon::now()->addDays($this->days), + 'created_at' => Carbon::now(), + ]; + } + if (!empty($data)) { + Invite::query()->insert($data); + } + do_log("success add $this->count temporary invite ($this->days days) to $uid"); + } catch (\Exception $exception) { + do_log("fail add $this->count temporary invite ($this->days days) to $uid: " . $exception->getMessage(), 'error'); + } + } + + } + +} diff --git a/app/Models/Invite.php b/app/Models/Invite.php index 91fc38f4..371aab30 100644 --- a/app/Models/Invite.php +++ b/app/Models/Invite.php @@ -9,6 +9,10 @@ class Invite extends NexusModel const VALID_YES = 1; const VALID_NO = 0; + protected $casts = [ + 'expired_at' => 'datetime', + ]; + public static $validInfo = [ self::VALID_NO => ['text' => 'No'], self::VALID_YES => ['text' => 'Yes'], diff --git a/app/Models/User.php b/app/Models/User.php index a50b0d47..86b314a2 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -360,6 +360,15 @@ class User extends Authenticatable implements FilamentUser, HasName return $this->belongsTo(User::class, 'invited_by'); } + public function temporary_invites() + { + return $this->hasMany(Invite::class, 'inviter') + ->where('invitee', '') + ->whereNotNull('expired_at') + ->where('expired_at', '>=', Carbon::now()) + ; + } + public function send_messages() { return $this->hasMany(Message::class, 'sender'); diff --git a/app/Repositories/ClaimRepository.php b/app/Repositories/ClaimRepository.php index 85716836..7f438912 100644 --- a/app/Repositories/ClaimRepository.php +++ b/app/Repositories/ClaimRepository.php @@ -153,6 +153,11 @@ class ClaimRepository extends BaseRepository public function settleUser($uid, $force = false, $test = false): bool { + $shouldDoSettle = apply_filter('user_should_do_claim_settle', true, $uid); + if (!$shouldDoSettle) { + do_log("uid: $uid, filter: user_should_do_claim_settle => false"); + return false; + } $user = User::query()->with('language')->findOrFail($uid); $list = Claim::query()->where('uid', $uid)->with(['snatch', 'torrent' => fn ($query) => $query->select(Torrent::$commentFields)])->get(); $now = Carbon::now(); diff --git a/app/Repositories/ToolRepository.php b/app/Repositories/ToolRepository.php index a63182ea..47d2aefa 100644 --- a/app/Repositories/ToolRepository.php +++ b/app/Repositories/ToolRepository.php @@ -1,6 +1,7 @@ 10) { + throw new \RuntimeException("deep: $deep > 10"); + } + if (count($hashArr) >= $total) { + return array_slice(array_values($hashArr), 0, $total); + } + for ($i = 0; $i < $left; $i++) { + $hash = Str::random(32); + $hashArr[$hash] = $hash; + } + $exists = Invite::query()->whereIn('hash', array_values($hashArr))->get(['id', 'hash']); + foreach($exists as $value) { + unset($hashArr[$value->hash]); + } + return $this->generateUniqueInviteHash($hashArr, $total, $total - count($hashArr), ++$deep); + + } } diff --git a/app/Repositories/UserRepository.php b/app/Repositories/UserRepository.php index c1d4183a..0f1b1e9e 100644 --- a/app/Repositories/UserRepository.php +++ b/app/Repositories/UserRepository.php @@ -6,6 +6,7 @@ use App\Exceptions\NexusException; use App\Http\Resources\ExamUserResource; use App\Http\Resources\UserResource; use App\Models\ExamUser; +use App\Models\Invite; use App\Models\Message; use App\Models\Setting; use App\Models\User; @@ -566,4 +567,81 @@ class UserRepository extends BaseRepository return true; } + public function addTemporaryInvite(User $operator, int $uid, string $action, int $count, int|null $days, string|null $reason = '') + { + do_log("uid: $uid, action: $action, count: $count, days: $days, reason: $reason"); + $action = strtolower($action); + if ($count <= 0 || ($action == 'increment' && $days <= 0)) { + throw new \InvalidArgumentException("days or count lte 0"); + } + $targetUser = User::query()->findOrFail($uid, User::$commonFields); + $this->checkPermission($operator, $targetUser); + $toolRep = new ToolRepository(); + $locale = $targetUser->locale; + + $changeType = nexus_trans("nexus.$action", [], $locale); + $subject = nexus_trans('message.temporary_invite_change.subject', ['change_type' => $changeType], $locale); + $body = nexus_trans('message.temporary_invite_change.body', [ + 'change_type' => $changeType, + 'count' => $count, + 'operator' => $operator->username, + 'reason' => $reason, + ], $locale); + $message = [ + 'sender' => 0, + 'receiver' => $targetUser->id, + 'subject' => $subject, + 'msg' => $body, + 'added' => Carbon::now(), + ]; + $inviteData = []; + if ($action == 'increment') { + $hashArr = $toolRep->generateUniqueInviteHash([], $count, $count); + foreach ($hashArr as $hash) { + $inviteData[] = [ + 'inviter' => $uid, + 'invitee' => '', + 'hash' => $hash, + 'valid' => 0, + 'expired_at' => Carbon::now()->addDays($days), + 'created_at' => Carbon::now(), + ]; + } + } + NexusDB::transaction(function () use ($uid, $message, $inviteData, $count) { + if (!empty($inviteData)) { + Invite::query()->insert($inviteData); + do_log("[INSERT TEMPORARY INVITE] to $uid, count: $count"); + } else { + Invite::query()->where('inviter', $uid) + ->where('invitee', '') + ->orderBy('expired_at', 'asc') + ->limit($count) + ->delete() + ; + do_log("[DELETE TEMPORARY INVITE] of $uid, count: $count"); + } + Message::add($message); + }); + return true; + } + + public function getInviteBtnText(int $uid) + { + if (Setting::get('main.invitesystem') != 'yes') { + throw new NexusException(nexus_trans('invite.send_deny_reasons.invite_system_closed')); + } + $permission = 'sendinvite'; + if (!user_can($permission, false, $uid)) { + $requireClass = get_setting("authority.$permission"); + throw new NexusException(nexus_trans('invite.send_deny_reasons.no_permission', ['class' => User::getClassText($requireClass)])); + } + $userInfo = User::query()->findOrFail($uid, User::$commonFields); + $temporaryInviteCount = $userInfo->temporary_invites()->count(); + if ($userInfo->invites + $temporaryInviteCount < 1) { + throw new NexusException(nexus_trans('invite.send_deny_reasons.invite_not_enough')); + } + return nexus_trans('invite.send_allow_text'); + } + } diff --git a/database/migrations/2022_12_10_034926_add_expired_at_to_invites_table.php b/database/migrations/2022_12_10_034926_add_expired_at_to_invites_table.php new file mode 100644 index 00000000..5f10cded --- /dev/null +++ b/database/migrations/2022_12_10_034926_add_expired_at_to_invites_table.php @@ -0,0 +1,35 @@ +dateTime('expired_at')->nullable(true)->index(); + $table->dateTime('created_at')->useCurrent(); + $table->index(['inviter'], 'idx_inviter'); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('invites', function (Blueprint $table) { + $table->dropColumn('expired_at', 'created_at'); + $table->dropIndex('idx_inviter'); + }); + } +}; diff --git a/database/migrations/2022_12_10_041706_change_invites_table_invitee_default_empty.php b/database/migrations/2022_12_10_041706_change_invites_table_invitee_default_empty.php new file mode 100644 index 00000000..c747e63d --- /dev/null +++ b/database/migrations/2022_12_10_041706_change_invites_table_invitee_default_empty.php @@ -0,0 +1,30 @@ +string('invitee')->default('')->change(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + // + } +}; diff --git a/include/cleanup.php b/include/cleanup.php index 54261a82..74990772 100644 --- a/include/cleanup.php +++ b/include/cleanup.php @@ -248,6 +248,7 @@ function docleanup($forceAll = 0, $printProgress = false) { set_time_limit(0); ignore_user_abort(1); $now = time(); + $nowStr = \Carbon\Carbon::now()->toDateTimeString(); do_log("start docleanup(), forceAll: $forceAll, printProgress: $printProgress, now: $now, " . date('Y-m-d H:i:s', $now)); //Priority Class 1: cleanup every 15 mins @@ -592,7 +593,7 @@ function docleanup($forceAll = 0, $printProgress = false) { //6.delete old invite codes $secs = $invite_timeout*24*60*60; // when? $dt = sqlesc(date("Y-m-d H:i:s",(TIMENOW - $secs))); // calculate date. - sql_query("DELETE FROM invites WHERE time_invited < $dt") or sqlerr(__FILE__, __LINE__); + sql_query("DELETE FROM invites WHERE ((time_invited < $dt and time_invited is not null and invitee != '') or (invitee = '' and expired_at < '$nowStr' and expired_at is not null))") or sqlerr(__FILE__, __LINE__); $log = "delete old invite codes"; do_log($log); if ($printProgress) { diff --git a/include/constants.php b/include/constants.php index d7a096a5..261acffe 100644 --- a/include/constants.php +++ b/include/constants.php @@ -1,6 +1,6 @@ ] []: '.$lang_functions['text_attended'].'', $attendance->points, $CURUSER['attendance_card']); }else{ printf(' %s', $lang_functions['text_attendance']);}?> - []: + []: where('inviter', $CURUSER['id'])->where('invitee', '')->where('expired_at', '>', now())->count())?> = \App\Models\User::CLASS_ADMINISTRATOR) printf('[%s]', nexus_env('FILAMENT_PATH', 'nexusphp'), $lang_functions['text_management_system'])?>
diff --git a/lang/chs/lang_invite.php b/lang/chs/lang_invite.php index 36e13820..b772d464 100644 --- a/lang/chs/lang_invite.php +++ b/lang/chs/lang_invite.php @@ -57,6 +57,11 @@ $lang_invite = array 'text_seed_torrent_bonus_per_hour_help' => '即做种积分,单纯做种的值,不考虑捐赠、官种、后宫等任何加成', 'text_seed_torrent_last_announce_at' => '最后做种汇报时间', 'text_enabled' => '启用', + 'text_tmp_status' => '临时邀请状态', + 'text_expired_at' => 'hash 过期时间', + 'text_permanent' => '永久有效邀请', + 'text_consume_invite' => '使用邀请', + 'text_temporary_left' => '%s个临时邀请', ); ?> diff --git a/lang/chs/lang_takeinvite.php b/lang/chs/lang_takeinvite.php index 3d111d9a..605dddc9 100644 --- a/lang/chs/lang_takeinvite.php +++ b/lang/chs/lang_takeinvite.php @@ -24,6 +24,9 @@ $lang_takeinvite = array 'std_no_invite' => "你没有剩余邀请名额。你怎么到这来的?", 'std_invitation_already_sent_to' => "发送失败!此邮箱", 'std_await_user_registeration' => "已收到过邀请函,请勿重复发送。请等待用户注册。", + 'std_must_select_invite' => "请选择要使用的邀请。", + 'hash_not_exists' => "hash 不存在。", + 'hash_expired' => "hash 已经过期。", ); ?> diff --git a/lang/cht/lang_invite.php b/lang/cht/lang_invite.php index 6351cdde..d12cce2e 100644 --- a/lang/cht/lang_invite.php +++ b/lang/cht/lang_invite.php @@ -57,6 +57,8 @@ $lang_invite = array 'text_seed_torrent_bonus_per_hour_help' => '即做種積分,單純做種的值,不考慮捐贈、官種、後宮等任何加成', 'text_seed_torrent_last_announce_at' => '最後做種匯報時間', 'text_enabled' => '啟用', + 'text_tmp_status' => '臨時邀請狀態', + 'text_expired_at' => '過期時間', ); ?> diff --git a/lang/en/lang_invite.php b/lang/en/lang_invite.php index 92a759c4..2a91905f 100644 --- a/lang/en/lang_invite.php +++ b/lang/en/lang_invite.php @@ -60,6 +60,8 @@ Best Regards, 'text_seed_torrent_bonus_per_hour_help' => 'That is, seed points, do not consider donations, official torrent, harems and any other additions', 'text_seed_torrent_last_announce_at' => 'Last announce time', 'text_enabled' => 'Enabled', + 'text_tmp_status' => 'Temporary invite status', + 'text_expired_at' => 'Expired at', ); ?> diff --git a/public/increment-bulk.php b/public/increment-bulk.php index dd60d4b0..14d1fea5 100644 --- a/public/increment-bulk.php +++ b/public/increment-bulk.php @@ -10,6 +10,7 @@ $validTypeMap = [ 'attendance_card' => 'Attend card', 'invites' => 'Invite', 'uploaded' => 'Upload', + 'tmp_invites' => 'Temporary invite', ]; $type = $_REQUEST['type'] ?? ''; stdhead("Add Bonus/Attend card/Invite/upload", false); @@ -49,6 +50,7 @@ $classes = array_chunk(\App\Models\User::$classes, 4, true); Amount + Duration Only required when Type = 'Temporary invite', Unit: Day Add to Class diff --git a/public/invite.php b/public/invite.php index db211012..5a0488ca 100644 --- a/public/invite.php +++ b/public/invite.php @@ -8,23 +8,31 @@ $id = intval($_GET["id"] ?? 0); $type = unesc($_GET["type"] ?? ''); $menuSelected = $_REQUEST['menu'] ?? 'invitee'; $pageSize = 50; +$userRep = new \App\Repositories\UserRepository(); function inviteMenu ($selected = "invitee") { - global $lang_invite, $id, $CURUSER, $invitesystem; + global $lang_invite, $id, $CURUSER, $invitesystem, $userRep; begin_main_frame("", false, "100%"); print ("
"); + print ("".$lang_invite['text_tmp_status'].""); + try { + $sendBtnText = $userRep->getInviteBtnText($CURUSER['id']); + $disabled = ''; + } catch (\Exception $exception) { + $sendBtnText = $exception->getMessage(); + $disabled = ' disabled'; } + print ("
"); end_main_frame(); } -if (($CURUSER['id'] != $id && !user_can('viewinvite')) || !is_valid_id($id)) -stderr($lang_invite['std_sorry'],$lang_invite['std_permission_denied']); $res = sql_query("SELECT username FROM users WHERE id = ".mysql_real_escape_string($id)) or sqlerr(); $user = mysql_fetch_assoc($res); +if (!$user) { + stderr($lang_invite['std_sorry'], 'Invalid id'); +} stdhead($lang_invite['head_invites']); print("
"); @@ -46,24 +54,34 @@ if ($inv["invites"] != 1){ } if ($type == 'new'){ - if (!user_can('sendinvite')) - stderr($lang_invite['std_sorry'],$lang_invite['std_only'].get_user_class_name($sendinvite_class,false,true,true).$lang_invite['std_or_above_can_invite'],false, false); + try { + $sendBtnText = $userRep->getInviteBtnText($CURUSER['id']); + } catch (\Exception $exception) { + stdmsg($lang_invite['std_sorry'],$exception->getMessage(). + " ".$lang_invite['here_to_go_back'],false); + print("
"); + stdfoot(); + die; + } registration_check('invitesystem',true,false); - if ($CURUSER['invites'] <= 0) { - stdmsg($lang_invite['std_sorry'],$lang_invite['std_no_invites_left']. - "".$lang_invite['here_to_go_back'],false); - print(""); - stdfoot(); - die; - } + $temporaryInvites = \App\Models\Invite::query()->where('inviter', $CURUSER['id']) + ->where('invitee', '') + ->where('expired_at', '>', now()) + ->orderBy('expired_at', 'asc') + ->get() + ; $invitation_body = $lang_invite['text_invitation_body'].$CURUSER['username']; //$invitation_body_insite = str_replace("
","\n",$invitation_body); + $inviteSelectOptions = ''; + foreach ($temporaryInvites as $tmp) { + $inviteSelectOptions .= sprintf('', $tmp->hash, $tmp->hash, $lang_invite['text_expired_at'], $tmp->expired_at); + } print("
". "". - "". + "". "". - "". + "". + "". "". "
".$lang_invite['text_invite_someone']."$SITENAME ({$inv['invites']}".$lang_invite['text_invitation'].$_s.$lang_invite['text_left'] .")
".$lang_invite['text_invite_someone']."$SITENAME ({$inv['invites']}".$lang_invite['text_invitation'].$_s.$lang_invite['text_left'] .' + '.sprintf($lang_invite['text_temporary_left'], $temporaryInvites->count()).")
".$lang_invite['text_email_address']."
".$lang_invite['text_email_address_note']."".($restrictemaildomain == 'yes' ? "
".$lang_invite['text_email_restriction_note'].allowedemails() : "")."
".$lang_invite['text_message']."
".$lang_invite['text_consume_invite']."
".$lang_invite['text_message']."
"); @@ -94,7 +112,9 @@ if ($type == 'new'){ $name, $_GET['status'] == $name ? ' selected' : '', $text ); } + $resetText = nexus_trans('label.reset'); + $submitText = nexus_trans('label.submit'); $filterForm = <<
@@ -112,7 +132,7 @@ if ($type == 'new'){ {$statusOptions}    - +
@@ -220,22 +240,32 @@ JS; } print(""); print("$pagertop"); - } elseif ($menuSelected == 'sent') { - $rul = sql_query("SELECT COUNT(*) FROM invites WHERE inviter =".mysql_real_escape_string($id)) or sqlerr(); + } elseif (in_array($menuSelected, ['sent', 'tmp'])) { + $whereStr = "inviter = " . sqlesc($id); + if ($menuSelected == 'sent') { + $whereStr .= " and invitee != ''"; + } elseif ($menuSelected == 'tmp') { + $whereStr .= " and invitee = '' and expired_at is not null"; + } + $rul = sql_query("SELECT COUNT(*) FROM invites WHERE $whereStr"); $arre = mysql_fetch_row($rul); $number1 = $arre[0]; - print(""); if(!$number1){ - print(""); + print(""); } else { list($pagertop, $pagerbottom, $limit) = pager($pageSize, $number1, "?id=$id&menu=$menuSelected&"); - $rer = sql_query("SELECT * FROM invites WHERE inviter = ".mysql_real_escape_string($id) . " $limit") or sqlerr(); + $rer = sql_query("SELECT * FROM invites WHERE $whereStr $limit") or sqlerr(); $num1 = mysql_num_rows($rer); - print(""); + print(""); + if ($menuSelected == 'tmp') { + print(""); + print(""); + } + print(""); for ($i = 0; $i < $num1; ++$i) { $arr1 = mysql_fetch_assoc($rer); @@ -254,6 +284,10 @@ JS; } else { $tr .= ""; } + if ($menuSelected == 'tmp') { + $tr .= ""; + $tr .= ""; + } $tr .= ""; print($tr); } diff --git a/public/take-increment-bulk.php b/public/take-increment-bulk.php index 9a570566..0c9e2e87 100644 --- a/public/take-increment-bulk.php +++ b/public/take-increment-bulk.php @@ -13,6 +13,7 @@ $validTypeMap = [ 'attendance_card' => 'Attend card', 'invites' => 'Invite', 'uploaded' => 'Upload', + 'tmp_invites' => 'Temporary invite', ]; $sender_id = ($_POST['sender'] == 'system' ? 0 : (int)$CURUSER['id']); $dt = sqlesc(date("Y-m-d H:i:s")); @@ -29,8 +30,9 @@ if (!isset($validTypeMap[$type])) { if ($type == 'uploaded') { $amount = sqlesc(getsize_int($amount,"G")); } +$isTypeTmpInvite = $type == 'tmp_invites'; $subject = trim($_POST['subject']); -$size = 10000; +$size = 2000; $page = 1; set_time_limit(300); $conditions = []; @@ -41,7 +43,12 @@ $conditions = apply_filter("role_query_conditions", $conditions, $_POST); if (empty($conditions)) { stderr("Error","No valid filter"); } +if ($isTypeTmpInvite && (empty($_POST['duration']) || $_POST['duration'] < 1)) { + stderr("Error","Invalid duration"); +} $whereStr = implode(' OR ', $conditions); +$phpPath = nexus_env('PHP_PATH', 'php'); +$webRoot = rtrim(ROOT_PATH, '/'); while (true) { $msgValues = $idArr = []; $offset = ($page - 1) * $size; @@ -54,10 +61,19 @@ while (true) { if (empty($idArr)) { break; } - $idStr = implode(', ', $idArr); + $idStr = implode(',', $idArr); + if ($isTypeTmpInvite) { + $command = sprintf( + '%s %s/artisan invite:tmp %s %s %s', + $phpPath, $webRoot, $idStr, $_POST['duration'], $amount + ); + $result = exec("$command 2>&1", $output, $result_code); + do_log(sprintf('command: %s, result_code: %s, result: %s, output: %s', $command, $result_code, $result, json_encode($output))); + } else { + sql_query("UPDATE users SET $type = $type + $amount WHERE id in ($idStr)"); + } $sql = "INSERT INTO messages (sender, receiver, added, subject, msg) VALUES " . implode(', ', $msgValues); sql_query($sql); - sql_query("UPDATE users SET $type = $type + $amount WHERE id in ($idStr)"); $page++; } diff --git a/public/takeinvite.php b/public/takeinvite.php index d0d95303..3147f3f3 100644 --- a/public/takeinvite.php +++ b/public/takeinvite.php @@ -3,10 +3,12 @@ require_once("../include/bittorrent.php"); dbconn(); require_once(get_langfile_path()); registration_check('invitesystem', true, false); -if (!user_can('sendinvite')) -stderr($lang_takeinvite['std_error'],$lang_takeinvite['std_invite_denied']); -if ($CURUSER['invites'] < 1) - stderr($lang_takeinvite['std_error'],$lang_takeinvite['std_no_invite']); +$userRep = new \App\Repositories\UserRepository(); +try { + $sendText = $userRep->getInviteBtnText($CURUSER['id']); +} catch (\Exception $exception) { + stderr($lang_takeinvite['std_error'], $exception->getMessage()); +} function bark($msg) { stdhead(); stdmsg($lang_takeinvite['head_invitation_failed'], $msg); @@ -43,7 +45,24 @@ if ($b[0] != 0) $ret = sql_query("SELECT username FROM users WHERE id = ".sqlesc($id)) or sqlerr(); $arr = mysql_fetch_assoc($ret); -$hash = md5(mt_rand(1,10000).$CURUSER['username'].TIMENOW.$CURUSER['passhash']); +if (empty($_POST['hash'])) { + bark($lang_takeinvite['std_must_select_invite']); +} +if ($_POST['hash'] == 'permanent') { + $hash = md5(mt_rand(1,10000).$CURUSER['username'].TIMENOW.$CURUSER['passhash']); +} else { + $hashRecord = \App\Models\Invite::query()->where('inviter', $CURUSER['id'])->where('hash', $_POST['hash'])->first(); + if (!$hashRecord) { + bark($lang_takeinvite['hash_not_exists']); + } + if ($hashRecord->invitee != '') { + bark('hash '.$lang_takeinvite['std_is_in_use']); + } + if ($hashRecord->expired_at->lt(now())) { + bark($lang_takeinvite['hash_expired']); + } + $hash = $_POST['hash']; +} $title = $SITENAME.$lang_takeinvite['mail_tilte']; @@ -60,8 +79,16 @@ EOD; $sendResult = sent_mail($email,$SITENAME,$SITEEMAIL,$title,$message,"invitesignup",false,false,''); //this email is sent only when someone give out an invitation if ($sendResult === true) { - sql_query("INSERT INTO invites (inviter, invitee, hash, time_invited) VALUES ('".mysql_real_escape_string($id)."', '".mysql_real_escape_string($email)."', '".mysql_real_escape_string($hash)."', " . sqlesc(date("Y-m-d H:i:s")) . ")"); - sql_query("UPDATE users SET invites = invites - 1 WHERE id = ".mysql_real_escape_string($id)) or sqlerr(__FILE__, __LINE__); + if (isset($hashRecord)) { + $hashRecord->update([ + 'invitee' => $email, + 'time_invited' => now(), + 'valid' => 1, + ]); + } else { + sql_query("INSERT INTO invites (inviter, invitee, hash, time_invited) VALUES ('".mysql_real_escape_string($id)."', '".mysql_real_escape_string($email)."', '".mysql_real_escape_string($hash)."', " . sqlesc(date("Y-m-d H:i:s")) . ")"); + sql_query("UPDATE users SET invites = invites - 1 WHERE id = ".mysql_real_escape_string($id)) or sqlerr(__FILE__, __LINE__); + } } header("Refresh: 0; url=invite.php?id=".htmlspecialchars($id)."&sent=1"); diff --git a/resources/lang/en/admin.php b/resources/lang/en/admin.php index 408fab56..ec1000d5 100644 --- a/resources/lang/en/admin.php +++ b/resources/lang/en/admin.php @@ -32,6 +32,7 @@ return [ 'category' => 'Categories', 'second_icon' => 'Second icons', 'torrent_operation_log' => 'Torrent operation logs', + 'invite' => 'Invites', ], 'resources' => [ 'agent_allow' => [ @@ -76,6 +77,8 @@ return [ 'grant_prop_form_duration' => 'Duration', 'grant_prop_form_duration_help' => 'Unit: days. If left blank, the user has it permanently. Note: There is no time limit for Name Change Card, ignore this value.' , 'confirm_bulk' => 'Bulk confirm', + 'change_bonus_etc_duration_label' => 'Duration', + 'change_bonus_etc_duration_help' => 'Required when adding temporary invitation, in days', ] ], 'exam_user' => [ diff --git a/resources/lang/en/invite.php b/resources/lang/en/invite.php index aa483fc4..e94e6ad6 100644 --- a/resources/lang/en/invite.php +++ b/resources/lang/en/invite.php @@ -2,4 +2,20 @@ return [ 'invalid_inviter' => 'Invalid inviter! The invite code is banned!', + 'fields' => [ + 'inviter' => 'Sender', + 'invitee' => 'Receive email', + 'time_invited' => 'Send time', + 'valid' => 'Valid', + 'invitee_register_uid' => 'Registered UID', + 'invitee_register_email' => 'Registered email', + 'invitee_register_username' => 'Registered username', + 'expired_at' => 'hash expired at', + ], + 'send_deny_reasons' => [ + 'invite_system_closed' => 'Invite system is closed', + 'no_permission' => 'Require :class or above to send invitations', + 'invite_not_enough' => 'Invites not enough', + ], + 'send_allow_text' => 'Invite someone', ]; diff --git a/resources/lang/en/label.php b/resources/lang/en/label.php index 56490bdd..24c620c6 100644 --- a/resources/lang/en/label.php +++ b/resources/lang/en/label.php @@ -102,6 +102,7 @@ return [ 'downloadpos' => 'Download privileges', 'parked' => 'Parked', 'offer_allowed_count' => 'Offer allowed count', + 'tmp_invites' => 'Temporary invite', ], 'medal' => [ 'label' => 'Medal', diff --git a/resources/lang/en/message.php b/resources/lang/en/message.php index 3d27859b..d1de2137 100644 --- a/resources/lang/en/message.php +++ b/resources/lang/en/message.php @@ -23,4 +23,8 @@ return [ 'subject' => 'Download permission restored', 'body' => 'Your download privileges restored, you can now download torrents. By: :operator', ], + 'temporary_invite_change' => [ + 'subject' => 'Temporary invite :change_type', + 'body' => 'Your temporary invite count had :change_type :count by :operator, reason: :reason.', + ], ]; diff --git a/resources/lang/en/nexus.php b/resources/lang/en/nexus.php index 2adb7113..7c553ea8 100644 --- a/resources/lang/en/nexus.php +++ b/resources/lang/en/nexus.php @@ -10,4 +10,6 @@ return [ ], 'select_all' => 'Select all', 'unselect_all' => 'Unselect all', + 'increment' => 'increment', + 'decrement' => 'decrement', ]; diff --git a/resources/lang/zh_CN/admin.php b/resources/lang/zh_CN/admin.php index 4b53c6d3..3a4a4024 100644 --- a/resources/lang/zh_CN/admin.php +++ b/resources/lang/zh_CN/admin.php @@ -30,6 +30,7 @@ return [ 'category' => '主分类', 'second_icon' => '第二图标', 'torrent_operation_log' => '种子操作记录', + 'invite' => '用户邀请', ], 'resources' => [ 'agent_allow' => [ @@ -74,6 +75,8 @@ return [ 'grant_prop_form_duration' => '有效时长', 'grant_prop_form_duration_help' => '单位:天。如果留空,用户永久拥有。注:改名卡没有时间限制,忽略该值。', 'confirm_bulk' => '批量确认', + 'change_bonus_etc_duration_label' => '有效期', + 'change_bonus_etc_duration_help' => '增加临时邀请时必须,单位:天', ] ], 'exam_user' => [ diff --git a/resources/lang/zh_CN/invite.php b/resources/lang/zh_CN/invite.php index c44640da..5cd8d943 100644 --- a/resources/lang/zh_CN/invite.php +++ b/resources/lang/zh_CN/invite.php @@ -2,4 +2,20 @@ return [ 'invalid_inviter' => '非法邀请者!此邀请码已被禁用!', + 'fields' => [ + 'inviter' => '发邀者', + 'invitee' => '接收邮箱', + 'time_invited' => '发邀时间', + 'valid' => '是否有效', + 'invitee_register_uid' => '注册用户 UID', + 'invitee_register_email' => '注册用户邮箱', + 'invitee_register_username' => '注册用户名', + 'expired_at' => 'hash 过期时间', + ], + 'send_deny_reasons' => [ + 'invite_system_closed' => '邀请系统已关闭', + 'no_permission' => ':class 或以上等级才可以发送邀请', + 'invite_not_enough' => '邀请数量不足', + ], + 'send_allow_text' => '邀请其他人', ]; diff --git a/resources/lang/zh_CN/label.php b/resources/lang/zh_CN/label.php index f4968b80..ea118162 100644 --- a/resources/lang/zh_CN/label.php +++ b/resources/lang/zh_CN/label.php @@ -102,6 +102,7 @@ return [ 'downloadpos' => '下载权限', 'parked' => '挂起', 'offer_allowed_count' => '候选通过数', + 'tmp_invites' => '临时邀请', ], 'medal' => [ 'label' => '勋章', diff --git a/resources/lang/zh_CN/message.php b/resources/lang/zh_CN/message.php index f030b00e..e47f47c2 100644 --- a/resources/lang/zh_CN/message.php +++ b/resources/lang/zh_CN/message.php @@ -23,4 +23,8 @@ return [ 'subject' => '下载权限恢复', 'body' => '你的下载权限恢复,你现在可以下载种子。By: :operator', ], + 'temporary_invite_change' => [ + 'subject' => '临时邀请:change_type', + 'body' => '你的临时邀请被管理员 :operator :change_type :count 个,理由::reason。', + ], ]; diff --git a/resources/lang/zh_CN/nexus.php b/resources/lang/zh_CN/nexus.php index 345c5645..24ce2f71 100644 --- a/resources/lang/zh_CN/nexus.php +++ b/resources/lang/zh_CN/nexus.php @@ -10,4 +10,6 @@ return [ ], 'select_all' => '全选', 'unselect_all' => '全不选', + 'increment' => '增加', + 'decrement' => '减少', ]; diff --git a/resources/lang/zh_TW/admin.php b/resources/lang/zh_TW/admin.php index ba39b69c..6f95043c 100644 --- a/resources/lang/zh_TW/admin.php +++ b/resources/lang/zh_TW/admin.php @@ -32,6 +32,7 @@ return [ 'category' => '主分類', 'second_icon' => '第二圖標', 'torrent_operation_log' => '種子操作記錄', + 'invite' => '用戶邀請', ], 'resources' => [ 'agent_allow' => [ @@ -76,6 +77,8 @@ return [ 'grant_prop_form_duration' => '有效時長', 'grant_prop_form_duration_help' => '單位:天。如果留空,用戶永久擁有。註:改名卡沒有時間限製,忽略該值。', 'confirm_bulk' => '批量確認', + 'change_bonus_etc_duration_label' => '有效期', + 'change_bonus_etc_duration_help' => '增加臨時邀請時必須,單位:天', ] ], 'exam_user' => [ diff --git a/resources/lang/zh_TW/invite.php b/resources/lang/zh_TW/invite.php index 888bbf5e..d2ab93ab 100644 --- a/resources/lang/zh_TW/invite.php +++ b/resources/lang/zh_TW/invite.php @@ -2,4 +2,20 @@ return [ 'invalid_inviter' => '非法邀請者!此邀請碼已被禁用!', + 'fields' => [ + 'inviter' => '發邀者', + 'invitee' => '接收郵箱', + 'time_invited' => '發邀時間', + 'valid' => '是否有效', + 'invitee_register_uid' => '註冊用戶 UID', + 'invitee_register_email' => '註冊用戶郵箱', + 'invitee_register_username' => '註冊用戶名', + 'expired_at' => 'hash 過期時間', + ], + 'send_deny_reasons' => [ + 'invite_system_closed' => '邀請系統已關閉', + 'no_permission' => ':class 或以上等級才可以發送邀請', + 'invite_not_enough' => '邀請數量不足', + ], + 'send_allow_text' => '邀請其他人', ]; diff --git a/resources/lang/zh_TW/label.php b/resources/lang/zh_TW/label.php index 7c6b348d..6573498c 100644 --- a/resources/lang/zh_TW/label.php +++ b/resources/lang/zh_TW/label.php @@ -102,6 +102,7 @@ return [ 'downloadpos' => '下載權限', 'parked' => '掛起', 'offer_allowed_count' => '候選通過數', + 'tmp_invites' => '臨時邀請', ], 'medal' => [ 'label' => '勛章', diff --git a/resources/lang/zh_TW/message.php b/resources/lang/zh_TW/message.php index 99aaceb2..bac6d977 100644 --- a/resources/lang/zh_TW/message.php +++ b/resources/lang/zh_TW/message.php @@ -22,4 +22,8 @@ return [ 'subject' => '下載權限恢復', 'body' => '你的下載權限恢復,你現在可以下載種子。By: :operator', ], + 'temporary_invite_change' => [ + 'subject' => '臨時邀請:change_type', + 'body' => '你的臨時邀請被管理員 :operator :change_type :count 個,理由::reason。', + ], ]; diff --git a/resources/lang/zh_TW/nexus.php b/resources/lang/zh_TW/nexus.php index 65d92c0f..4151b129 100644 --- a/resources/lang/zh_TW/nexus.php +++ b/resources/lang/zh_TW/nexus.php @@ -10,4 +10,6 @@ return [ ], 'select_all' => '全選', 'unselect_all' => '全不選', + 'increment' => '增加', + 'decrement' => '減少', ]; diff --git a/resources/views/filament/resources/user/user-resource/pages/user-profile.blade.php b/resources/views/filament/resources/user/user-resource/pages/user-profile.blade.php index faf61d4a..1ce33599 100644 --- a/resources/views/filament/resources/user/user-resource/pages/user-profile.blade.php +++ b/resources/views/filament/resources/user/user-resource/pages/user-profile.blade.php @@ -87,7 +87,7 @@ - +
".$lang_invite['text_no_invitation_sent']."
".$lang_functions['text_none']."
".$lang_invite['text_email']."".$lang_invite['text_hash']."".$lang_invite['text_send_date']."".$lang_invite['text_hash_status']."".$lang_invite['text_invitee_user']."
".$lang_invite['text_email']."".$lang_invite['text_hash']."".$lang_invite['text_send_date']."".$lang_invite['text_hash_status']."".$lang_invite['text_invitee_user']."".$lang_invite['text_expired_at']."".nexus_trans('label.created_at')."
{$arr1['expired_at']}{$arr1['created_at']}
{{ __('label.user.invites') }}{{$record->invites}}{{sprintf('%s(%s)', $record->invites, $temporary_invite_count)}}