├── vue ├── css │ ├── app.0e433876.css │ ├── 2.a52e3e65.css │ ├── 3.5cb2efc2.css │ └── 3.a52e3e65.css ├── favicon.ico ├── icons │ ├── favicon-16x16.png │ ├── favicon-32x32.png │ ├── favicon-96x96.png │ └── favicon-128x128.png ├── fonts │ ├── KFOmCnqEu92Fr1Mu4mxM.49ae34d4.woff │ ├── KFOkCnqEu92Fr1MmgVxIIzQ.a45108d3.woff │ ├── KFOlCnqEu92Fr1MmEU9fBBc-.cea99d3e.woff │ ├── KFOlCnqEu92Fr1MmSU5fBBc-.865f928c.woff │ ├── KFOlCnqEu92Fr1MmWUlfBBc-.2267169e.woff │ ├── KFOlCnqEu92Fr1MmYUtfBBc-.bac8362e.woff │ ├── flUhRq6tzZclQEJ-Vdg-IuiaDsNa.0cd122a9.woff │ └── flUhRq6tzZclQEJ-Vdg-IuiaDsNcIhQ8tQ.2987c5cc.woff2 ├── js │ ├── 3.74e3fcfd.js │ ├── 4.d4fc47ff.js │ ├── 6.642a8c9d.js │ ├── 7.2444fac4.js │ ├── 7.a056b5fa.js │ ├── 8.a093542b.js │ ├── 5.35de94fb.js │ ├── 6.f72bbf7f.js │ ├── 4.cf5327cd.js │ ├── 5.69a84b0f.js │ ├── 2.c47ca998.js │ ├── 3.39f6dfb4.js │ ├── 3.0457a8b4.js │ ├── 2.59ffaecf.js │ ├── 2.4383789c.js │ ├── app.64de2849.js │ ├── app.94c80ae2.js │ └── app.9d74b819.js ├── index.html └── img │ └── quasar-logo-full.68ae1645.svg ├── requirements.txt ├── friend.py ├── filter.json ├── constant.py ├── schedule.py ├── web_activate.py ├── __init__.py ├── activate ├── activate.html ├── js │ └── index.js └── css │ └── style.css ├── authMS.py.example ├── web_server.py ├── README.md ├── card.py ├── group.py ├── admin.py ├── util.py └── LICENSE /vue/css/app.0e433876.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | sqlitedict~=1.6.0 -------------------------------------------------------------------------------- /vue/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pcrbot/authMS/HEAD/vue/favicon.ico -------------------------------------------------------------------------------- /vue/icons/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pcrbot/authMS/HEAD/vue/icons/favicon-16x16.png -------------------------------------------------------------------------------- /vue/icons/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pcrbot/authMS/HEAD/vue/icons/favicon-32x32.png -------------------------------------------------------------------------------- /vue/icons/favicon-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pcrbot/authMS/HEAD/vue/icons/favicon-96x96.png -------------------------------------------------------------------------------- /vue/icons/favicon-128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pcrbot/authMS/HEAD/vue/icons/favicon-128x128.png -------------------------------------------------------------------------------- /vue/css/2.a52e3e65.css: -------------------------------------------------------------------------------- 1 | .doc-page{padding:16px 46px;font-weight:300;max-width:900px;margin-left:auto;margin-right:auto} -------------------------------------------------------------------------------- /vue/css/3.5cb2efc2.css: -------------------------------------------------------------------------------- 1 | .doc-page{padding:16px 46px;font-weight:300;max-width:900px;margin-left:auto;margin-right:auto} -------------------------------------------------------------------------------- /vue/css/3.a52e3e65.css: -------------------------------------------------------------------------------- 1 | .doc-page{padding:16px 46px;font-weight:300;max-width:900px;margin-left:auto;margin-right:auto} -------------------------------------------------------------------------------- /vue/fonts/KFOmCnqEu92Fr1Mu4mxM.49ae34d4.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pcrbot/authMS/HEAD/vue/fonts/KFOmCnqEu92Fr1Mu4mxM.49ae34d4.woff -------------------------------------------------------------------------------- /vue/fonts/KFOkCnqEu92Fr1MmgVxIIzQ.a45108d3.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pcrbot/authMS/HEAD/vue/fonts/KFOkCnqEu92Fr1MmgVxIIzQ.a45108d3.woff -------------------------------------------------------------------------------- /vue/fonts/KFOlCnqEu92Fr1MmEU9fBBc-.cea99d3e.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pcrbot/authMS/HEAD/vue/fonts/KFOlCnqEu92Fr1MmEU9fBBc-.cea99d3e.woff -------------------------------------------------------------------------------- /vue/fonts/KFOlCnqEu92Fr1MmSU5fBBc-.865f928c.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pcrbot/authMS/HEAD/vue/fonts/KFOlCnqEu92Fr1MmSU5fBBc-.865f928c.woff -------------------------------------------------------------------------------- /vue/fonts/KFOlCnqEu92Fr1MmWUlfBBc-.2267169e.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pcrbot/authMS/HEAD/vue/fonts/KFOlCnqEu92Fr1MmWUlfBBc-.2267169e.woff -------------------------------------------------------------------------------- /vue/fonts/KFOlCnqEu92Fr1MmYUtfBBc-.bac8362e.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pcrbot/authMS/HEAD/vue/fonts/KFOlCnqEu92Fr1MmYUtfBBc-.bac8362e.woff -------------------------------------------------------------------------------- /vue/fonts/flUhRq6tzZclQEJ-Vdg-IuiaDsNa.0cd122a9.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pcrbot/authMS/HEAD/vue/fonts/flUhRq6tzZclQEJ-Vdg-IuiaDsNa.0cd122a9.woff -------------------------------------------------------------------------------- /vue/fonts/flUhRq6tzZclQEJ-Vdg-IuiaDsNcIhQ8tQ.2987c5cc.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pcrbot/authMS/HEAD/vue/fonts/flUhRq6tzZclQEJ-Vdg-IuiaDsNcIhQ8tQ.2987c5cc.woff2 -------------------------------------------------------------------------------- /vue/js/3.74e3fcfd.js: -------------------------------------------------------------------------------- 1 | (window["webpackJsonp"]=window["webpackJsonp"]||[]).push([[3],{"8b24":function(e,a,n){"use strict";n.r(a);var t=function(){var e=this,a=e.$createElement,t=e._self._c||a;return t("q-page",{staticClass:"flex flex-center"},[t("img",{attrs:{alt:"Quasar logo",src:n("d3d6")}})])},s=[],l={name:"PageIndex"},o=l,r=n("2877"),c=n("9989"),u=n("eebe"),i=n.n(u),p=Object(r["a"])(o,t,s,!1,null,null,null);a["default"]=p.exports;i()(p,"components",{QPage:c["a"]})},d3d6:function(e,a,n){e.exports=n.p+"img/quasar-logo-full.68ae1645.svg"}}]); -------------------------------------------------------------------------------- /vue/js/4.d4fc47ff.js: -------------------------------------------------------------------------------- 1 | (window["webpackJsonp"]=window["webpackJsonp"]||[]).push([[4],{"8b24":function(e,a,n){"use strict";n.r(a);var t=function(){var e=this,a=e.$createElement,t=e._self._c||a;return t("q-page",{staticClass:"flex flex-center"},[t("img",{attrs:{alt:"Quasar logo",src:n("d3d6")}})])},s=[],l={name:"PageIndex"},o=l,r=n("2877"),c=n("9989"),u=n("eebe"),i=n.n(u),p=Object(r["a"])(o,t,s,!1,null,null,null);a["default"]=p.exports;i()(p,"components",{QPage:c["a"]})},d3d6:function(e,a,n){e.exports=n.p+"img/quasar-logo-full.68ae1645.svg"}}]); -------------------------------------------------------------------------------- /friend.py: -------------------------------------------------------------------------------- 1 | from nonebot import on_request 2 | 3 | import hoshino 4 | 5 | from .constant import config 6 | from . import util 7 | 8 | 9 | 10 | @on_request('friend') 11 | async def friend_approve(session): 12 | if config.FRIEND_APPROVE: 13 | util.log(f'已自动接受来自{session.event.user_id}的好友请求','friend_add') 14 | hoshino.logger.info(f'已自动接受来自{session.event.user_id}的好友请求') 15 | await session.approve() 16 | else: 17 | util.log(f'收到来自{session.event.user_id}的好友请求','friend_add') 18 | hoshino.logger.info(f'收到来自{session.event.user_id}的好友请求') 19 | -------------------------------------------------------------------------------- /filter.json: -------------------------------------------------------------------------------- 1 | { 2 | ".or": [ 3 | { 4 | "group_id": { 5 | ".in": [ 6 | ] 7 | } 8 | }, 9 | { 10 | "post_type": { 11 | ".regex": "notice|request|meta_event" 12 | } 13 | }, 14 | { 15 | "raw_message": { 16 | ".or": [ 17 | { 18 | ".contains": "查询授权" 19 | }, 20 | { 21 | ".regex": "^充值ֵ" 22 | } 23 | ] 24 | } 25 | }, 26 | { 27 | "message_type": "private" 28 | } 29 | ] 30 | } -------------------------------------------------------------------------------- /constant.py: -------------------------------------------------------------------------------- 1 | from sqlitedict import SqliteDict 2 | 3 | import os 4 | import hoshino 5 | 6 | 7 | __version__ = '0.2.1.6' 8 | 9 | try: 10 | config = hoshino.config.authMS.auth_config 11 | except: 12 | # 保不准哪个憨憨又不读README呢 13 | hoshino.logger.error('authMS无配置文件!请仔细阅读README') 14 | 15 | 16 | if hoshino.config.authMS.auth_config.ENABLE_COM: 17 | path_first = hoshino.config.authMS.auth_config.DB_PATH 18 | else: 19 | path_first = '' 20 | 21 | 22 | key_dict = SqliteDict(os.path.join(path_first, 'key.sqlite'), autocommit=True) 23 | group_dict = SqliteDict(os.path.join(path_first, 'group.sqlite'), autocommit=True) 24 | trial_list = SqliteDict(os.path.join(path_first, 'trial.sqlite'), autocommit=True) # 试用列表 25 | 26 | 27 | -------------------------------------------------------------------------------- /vue/js/6.642a8c9d.js: -------------------------------------------------------------------------------- 1 | (window["webpackJsonp"]=window["webpackJsonp"]||[]).push([[6],{e51e:function(t,e,n){"use strict";n.r(e);var l=function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("div",{staticClass:"fullscreen bg-blue text-white text-center q-pa-md flex flex-center"},[n("div",[n("div",{staticStyle:{"font-size":"30vh"}},[t._v("\n 404\n ")]),n("div",{staticClass:"text-h2",staticStyle:{opacity:".4"}},[t._v("\n Oops. Nothing here...\n ")]),n("q-btn",{staticClass:"q-mt-xl",attrs:{color:"white","text-color":"blue",unelevated:"",to:"/",label:"Go Home","no-caps":""}})],1)])},s=[],a={name:"Error404"},c=a,o=n("2877"),i=n("9c40"),r=n("eebe"),u=n.n(r),p=Object(o["a"])(c,l,s,!1,null,null,null);e["default"]=p.exports;u()(p,"components",{QBtn:i["a"]})}}]); -------------------------------------------------------------------------------- /vue/js/7.2444fac4.js: -------------------------------------------------------------------------------- 1 | (window["webpackJsonp"]=window["webpackJsonp"]||[]).push([[7],{e51e:function(t,e,n){"use strict";n.r(e);var l=function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("div",{staticClass:"fullscreen bg-blue text-white text-center q-pa-md flex flex-center"},[n("div",[n("div",{staticStyle:{"font-size":"30vh"}},[t._v("\n 404\n ")]),n("div",{staticClass:"text-h2",staticStyle:{opacity:".4"}},[t._v("\n Oops. Nothing here...\n ")]),n("q-btn",{staticClass:"q-mt-xl",attrs:{color:"white","text-color":"blue",unelevated:"",to:"/",label:"Go Home","no-caps":""}})],1)])},s=[],a={name:"Error404"},c=a,o=n("2877"),i=n("9c40"),r=n("eebe"),u=n.n(r),p=Object(o["a"])(c,l,s,!1,null,null,null);e["default"]=p.exports;u()(p,"components",{QBtn:i["a"]})}}]); -------------------------------------------------------------------------------- /schedule.py: -------------------------------------------------------------------------------- 1 | from nonebot import scheduler 2 | 3 | import pytz 4 | import datetime 5 | 6 | from .constant import config 7 | from .group import check_auth 8 | 9 | tz = pytz.timezone('Asia/Shanghai') 10 | 11 | 12 | @scheduler.scheduled_job('cron', hour='*', minute='02') 13 | async def check_auth_sdj(): 14 | ''' 15 | 自动检查Bot已加入的群的授权是否过期 \n 16 | 注意只会检查已加入的群, 未加入而有授权的群, 不会被检查, 但是授权时间照样流逝(-1s), 在加入该群后才会在日志显示 \n 17 | 例如1月5日给A群3天授权, 那么当A群7日邀请时剩余授权时间会是1天, 9日邀请时会拒绝加群 \n 18 | v0.1.1后新增特性, 在配置ENABLE_AUTH为0, 则不会自动检查, 并且整个授权系统不生效, 但是可以充值, 生成卡密等, 19 | 以度过刚装上授权系统后的过渡期 20 | ''' 21 | now = datetime.datetime.now(tz) 22 | hour_now = now.hour 23 | if hour_now % config.FREQUENCY != 0: 24 | return 25 | if not config.ENABLE_AUTH: 26 | return 27 | await check_auth() 28 | -------------------------------------------------------------------------------- /vue/index.html: -------------------------------------------------------------------------------- 1 | Hoshino-Auth
-------------------------------------------------------------------------------- /web_activate.py: -------------------------------------------------------------------------------- 1 | from quart import request, Blueprint, jsonify, render_template 2 | 3 | import nonebot 4 | 5 | from . import util 6 | 7 | 8 | 9 | activate = Blueprint('activate', __name__, url_prefix='/activate', template_folder="./activate" 10 | , static_folder='./activate', static_url_path='') 11 | bot = nonebot.get_bot() 12 | app = bot.server_app 13 | 14 | 15 | @activate.route("/", methods=["GET", "POST"]) 16 | async def activate_group(): 17 | if request.method == "GET": 18 | if key := request.args.get("key"): 19 | if gid := request.args.get('group'): 20 | group_id = int(gid) 21 | days = util.query_key(key) 22 | result = await util.reg_group(group_id, key) 23 | if result: 24 | log_info = f'卡密{key}通过网页被激活\n为群聊{group_id}增加了{days}天授权时长' 25 | util.log(log_info,'card_use') 26 | await util.notify_master(log_info) 27 | return await render_template("activate.html") 28 | -------------------------------------------------------------------------------- /__init__.py: -------------------------------------------------------------------------------- 1 | from nonebot import on_command, get_bot 2 | 3 | import hoshino 4 | 5 | from .constant import config, __version__ 6 | from .web_server import auth 7 | from .web_activate import activate 8 | 9 | 10 | 11 | if config.ENABLE_WEB: 12 | # 开启web请修改authMS.py 13 | app = get_bot().server_app 14 | app.register_blueprint(auth) 15 | app.register_blueprint(activate) 16 | 17 | 18 | @on_command('充值帮助', aliases=('我要充钱','续费帮助','我要续费','👴要充钱'), only_to_me=False) 19 | async def reg_help_chat(session): 20 | if session.event.detail_type == 'private': 21 | msg = config.REG_HELP_PRIVATE 22 | else: 23 | msg = config.REG_HELP_GROUP 24 | #else: 25 | # 新版QQ已不在有discuss, 所有多人聊天都是群消息 26 | # return 27 | await session.finish(msg) 28 | 29 | 30 | @on_command('管理员帮助', only_to_me=False) 31 | async def master_help_chat(session): 32 | if session.event.detail_type == 'group': 33 | return 34 | if session.event.user_id not in hoshino.config.SUPERUSERS: 35 | await session.finish('只有主人才能查看此页帮助') 36 | await session.finish(config.ADMIN_HELP) 37 | 38 | 39 | @on_command('授权系统版本', only_to_me=True) 40 | async def check_new_ver_chat(session): 41 | if session.event.user_id not in hoshino.config.SUPERUSERS: 42 | return 43 | await session.finish(f'授权系统当前版本v{__version__}') 44 | -------------------------------------------------------------------------------- /vue/js/7.a056b5fa.js: -------------------------------------------------------------------------------- 1 | (window["webpackJsonp"]=window["webpackJsonp"]||[]).push([[7],{d684:function(t,e,a){"use strict";a.r(e);var s=function(){var t=this,e=t.$createElement,a=t._self._c||e;return a("div",{staticClass:"q-ma-xl"},[a("span",{staticStyle:{"font-size":"28px"}},[t._v("授权列表")]),a("div",{staticClass:"q-pa-md"},[a("div",{staticClass:"row justify-center q-gutter-sm",staticStyle:{width:"auto"}},t._l(t.groupList,(function(e,s){return a("q-intersection",{key:s,staticClass:"example-item",attrs:{once:"",transition:"scale"}},[a("q-card",{staticClass:"bg-primary text-white",staticStyle:{width:"180px"}},[a("br"),a("div",{staticClass:"text-bold text-center"},[t._v("\n 群号: "+t._s(e.gid)+"\n ")]),a("br"),a("q-card-section",{staticClass:"justify-center bg-teal-14",staticStyle:{"vertical-align":"middle"}},[t._v("\n 授权截止至: "+t._s(e.deadline)+"\n ")])],1)],1)})),1)])])},i=[],n=a("18d6"),r={name:"Group",data(){return{groupList:[{gid:123456,deadline:"2020-12-20"}]}},methods:{updateGroup(){this.$axios.get("/get/group?password="+n["a"].getItem("password")).then(t=>{this.groupList=t.data})}},created(){this.updateGroup()}},c=r,d=a("2877"),o=a("ad56"),l=a("f09f"),p=a("a370"),u=a("eebe"),g=a.n(u),f=Object(d["a"])(c,s,i,!1,null,"5aeff71e",null);e["default"]=f.exports;g()(f,"components",{QIntersection:o["a"],QCard:l["a"],QCardSection:p["a"]})}}]); -------------------------------------------------------------------------------- /vue/js/8.a093542b.js: -------------------------------------------------------------------------------- 1 | (window["webpackJsonp"]=window["webpackJsonp"]||[]).push([[8],{"013f":function(a,t,s){"use strict";s.r(t);var e=function(){var a=this,t=a.$createElement,s=a._self._c||t;return s("q-page",{staticClass:"flex flex-center"},[s("q-dialog",{attrs:{persistent:""},model:{value:a.alert,callback:function(t){a.alert=t},expression:"alert"}},[s("q-card",{staticClass:"flex flex-center"},[s("q-card-section",[s("div",{staticClass:"text-h6"},[a._v("请输入管理员密码")])]),s("q-card-section",{staticClass:"q-pt-none"},[s("q-input",{attrs:{type:"password"},model:{value:a.password,callback:function(t){a.password=t},expression:"password"}})],1),s("q-card-actions",{attrs:{align:"right"}},[s("q-btn",{attrs:{flat:"",label:"确定",color:"primary"},on:{click:a.submit}})],1)],1)],1)],1)},o=[],l=(s("5319"),{name:"Login",data(){return{password:"",alert:!0}},methods:{submit(){this.$axios.post("login?password="+this.password).then(a=>{console.log(a),"success"===a.data&&(this.$q.localStorage.set("login",!0),this.$q.localStorage.set("password",this.password),this.$router.replace("/"))}).catch(a=>{console.log(a)})}}}),r=l,n=s("2877"),c=s("9989"),i=s("24e8"),p=s("f09f"),d=s("a370"),u=s("27f9"),f=s("4b7e"),w=s("9c40"),g=s("eebe"),h=s.n(g),b=Object(n["a"])(r,e,o,!1,null,null,null);t["default"]=b.exports;h()(b,"components",{QPage:c["a"],QDialog:i["a"],QCard:p["a"],QCardSection:d["a"],QInput:u["a"],QCardActions:f["a"],QBtn:w["a"]})}}]); -------------------------------------------------------------------------------- /activate/activate.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 授权激活 6 | 7 | 8 | 9 |
10 |
11 |
12 |
13 |
14 | 15 | 16 | 17 |
18 |
19 |
20 |

授权激活

21 | 22 |
23 | 24 |
授权码
25 |
26 |
27 |
28 | 29 |
群号
30 |
31 |
32 | 33 | 34 |
35 | 36 |
37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /vue/js/5.35de94fb.js: -------------------------------------------------------------------------------- 1 | (window["webpackJsonp"]=window["webpackJsonp"]||[]).push([[5],{"7aa6":function(e,t,s){"use strict";s.r(t);var a=function(){var e=this,t=e.$createElement,s=e._self._c||t;return s("q-page",{staticClass:"flex flex-center"},[s("q-dialog",{on:{hide:e.onHide},model:{value:e.alert,callback:function(t){e.alert=t},expression:"alert"}},[s("q-card",{staticClass:"flex flex-center"},[s("div",{staticClass:"q-pa-md",staticStyle:{width:"480px"}},[s("q-form",{staticClass:"q-gutter-md"},[s("q-input",{attrs:{filled:"",label:"注册码","lazy-rules":"",rules:[function(e){return e&&e.length>0||"请输入注册码!"}]},model:{value:e.key,callback:function(t){e.key=t},expression:"key"}}),s("q-input",{attrs:{filled:"",type:"number",label:"群号*","lazy-rules":"",rules:[function(e){return null!==e&&""!==e||"请输入群号!"}]},model:{value:e.gid,callback:function(t){e.gid=t},expression:"gid"}}),s("div",{attrs:{align:"center"}},[s("q-btn",{attrs:{label:"Submit",color:"primary"},on:{click:e.onSubmit}}),s("q-btn",{staticClass:"q-ml-sm",attrs:{label:"Reset",color:"primary",flat:""},on:{click:e.onReset}})],1)],1)],1)])],1)],1)},i=[],l=(s("5319"),{name:"Login",data(){return{key:"",gid:0,alert:!0}},methods:{message(e){this.$q.dialog({title:"消息",message:e}).onOk(()=>{}).onCancel(()=>{}).onDismiss(()=>{})},submit(){this.$axios.post("activate?key="+this.key+"&gid="+this.gid).then(e=>{"success"===e.data?(this.message("已为您的群"+this.gid.toString()+"注册成功!"),this.$router.replace("/")):this.message("注册失败!")}).catch(e=>{console.log(e)})},onSubmit(){this.$q.dialog({title:"Confirm",message:"确定为群"+this.gid.toString()+"注册?",cancel:!0,persistent:!0}).onOk(()=>{}).onOk(()=>{this.submit()}).onCancel(()=>{}).onDismiss(()=>{})},onReset(){this.gid=0,this.key=""},onHide(){this.$router.replace("/")}}}),n=l,o=s("2877"),r=s("9989"),c=s("24e8"),u=s("f09f"),d=s("0378"),m=s("27f9"),g=s("9c40"),p=s("eebe"),h=s.n(p),f=Object(o["a"])(n,a,i,!1,null,null,null);t["default"]=f.exports;h()(f,"components",{QPage:r["a"],QDialog:c["a"],QCard:u["a"],QForm:d["a"],QInput:m["a"],QBtn:g["a"]})}}]); -------------------------------------------------------------------------------- /vue/js/6.f72bbf7f.js: -------------------------------------------------------------------------------- 1 | (window["webpackJsonp"]=window["webpackJsonp"]||[]).push([[6],{"7aa6":function(e,t,s){"use strict";s.r(t);var a=function(){var e=this,t=e.$createElement,s=e._self._c||t;return s("q-page",{staticClass:"flex flex-center"},[s("q-dialog",{on:{hide:e.onHide},model:{value:e.alert,callback:function(t){e.alert=t},expression:"alert"}},[s("q-card",{staticClass:"flex flex-center"},[s("div",{staticClass:"q-pa-md",staticStyle:{width:"480px"}},[s("q-form",{staticClass:"q-gutter-md"},[s("q-input",{attrs:{filled:"",label:"注册码","lazy-rules":"",rules:[function(e){return e&&e.length>0||"请输入注册码!"}]},model:{value:e.key,callback:function(t){e.key=t},expression:"key"}}),s("q-input",{attrs:{filled:"",type:"number",label:"群号*","lazy-rules":"",rules:[function(e){return null!==e&&""!==e||"请输入群号!"}]},model:{value:e.gid,callback:function(t){e.gid=t},expression:"gid"}}),s("div",{attrs:{align:"center"}},[s("q-btn",{attrs:{label:"Submit",color:"primary"},on:{click:e.onSubmit}}),s("q-btn",{staticClass:"q-ml-sm",attrs:{label:"Reset",color:"primary",flat:""},on:{click:e.onReset}})],1)],1)],1)])],1)],1)},i=[],l=(s("5319"),{name:"Login",data(){return{key:"",gid:0,alert:!0}},methods:{message(e){this.$q.dialog({title:"消息",message:e}).onOk(()=>{}).onCancel(()=>{}).onDismiss(()=>{})},submit(){this.$axios.post("activate?key="+this.key+"&gid="+this.gid).then(e=>{"success"===e.data?(this.message("已为您的群"+this.gid.toString()+"注册成功!"),this.$router.replace("/")):this.message("注册失败!")}).catch(e=>{console.log(e)})},onSubmit(){this.$q.dialog({title:"Confirm",message:"确定为群"+this.gid.toString()+"注册?",cancel:!0,persistent:!0}).onOk(()=>{}).onOk(()=>{this.submit()}).onCancel(()=>{}).onDismiss(()=>{})},onReset(){this.gid=0,this.key=""},onHide(){this.$router.replace("/")}}}),n=l,o=s("2877"),r=s("9989"),c=s("24e8"),u=s("f09f"),d=s("0378"),m=s("27f9"),g=s("9c40"),p=s("eebe"),h=s.n(p),f=Object(o["a"])(n,a,i,!1,null,null,null);t["default"]=f.exports;h()(f,"components",{QPage:r["a"],QDialog:c["a"],QCard:u["a"],QForm:d["a"],QInput:m["a"],QBtn:g["a"]})}}]); -------------------------------------------------------------------------------- /activate/js/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var placeholders = document.querySelectorAll('.styled-input__placeholder-text'), 4 | inputs = document.querySelectorAll('.styled-input__input'); 5 | 6 | placeholders.forEach(function (el, i) { 7 | var value = el.innerText, 8 | html = ''; 9 | for (var _iterator = value, _isArray = Array.isArray(_iterator), _i = 0, _iterator = _isArray ? _iterator : _iterator[Symbol.iterator]();;) { 10 | var _ref; 11 | 12 | if (_isArray) { 13 | if (_i >= _iterator.length) break; 14 | _ref = _iterator[_i++]; 15 | } else { 16 | _i = _iterator.next(); 17 | if (_i.done) break; 18 | _ref = _i.value; 19 | } 20 | 21 | var w = _ref; 22 | 23 | if (!value) value = ' '; 24 | html += '' + w + ''; 25 | } 26 | el.innerHTML = html; 27 | }); 28 | 29 | inputs.forEach(function (el) { 30 | var parent = el.parentNode; 31 | el.addEventListener('focus', function () { 32 | parent.classList.add('filled'); 33 | placeholderAnimationIn(parent, true); 34 | }, false); 35 | el.addEventListener('blur', function () { 36 | if (el.value.length) return; 37 | parent.classList.remove('filled'); 38 | placeholderAnimationIn(parent, false); 39 | }, false); 40 | }); 41 | 42 | function placeholderAnimationIn(parent, action) { 43 | var act = action ? 'add' : 'remove'; 44 | var letters = parent.querySelectorAll('.letter'); 45 | letters = [].slice.call(letters, 0); 46 | if (!action) letters = letters.reverse(); 47 | letters.forEach(function (el, i) { 48 | setTimeout(function () { 49 | var contains = parent.classList.contains('filled'); 50 | if (action && !contains || !action && contains) return; 51 | el.classList[act]('active'); 52 | }, 50 * i); 53 | }); 54 | } 55 | 56 | setTimeout(function () { 57 | document.body.classList.add('on-start'); 58 | }, 100); 59 | 60 | setTimeout(function () { 61 | document.body.classList.add('document-loaded'); 62 | }, 1800); 63 | -------------------------------------------------------------------------------- /vue/js/4.cf5327cd.js: -------------------------------------------------------------------------------- 1 | (window["webpackJsonp"]=window["webpackJsonp"]||[]).push([[4],{"713b":function(e,t,n){"use strict";n.r(t);var a=function(){var e=this,t=e.$createElement,n=e._self._c||t;return n("q-layout",{attrs:{view:"lHh Lpr lFf"}},[n("q-header",{attrs:{elevated:""}},[n("q-toolbar",[n("q-btn",{attrs:{flat:"",dense:"",round:"",icon:"menu","aria-label":"Menu"},on:{click:function(t){e.leftDrawerOpen=!e.leftDrawerOpen}}}),n("q-toolbar-title",[e._v("\n Hoshino授权管理系统\n ")])],1)],1),n("q-drawer",{attrs:{"show-if-above":"",bordered:"","content-class":"bg-grey-1"},model:{value:e.leftDrawerOpen,callback:function(t){e.leftDrawerOpen=t},expression:"leftDrawerOpen"}},[n("q-list",[n("q-item-label",{staticClass:"text-grey-8",attrs:{header:""}},[e._v("\n 导航\n ")]),e._l(e.essentialLinks,(function(t){return n("EssentialLink",e._b({key:t.title},"EssentialLink",t,!1))}))],2)],1),n("q-page-container",[n("router-view")],1)],1)},l=[],i=function(){var e=this,t=e.$createElement,n=e._self._c||t;return n("q-item",{attrs:{clickable:"",tag:"a"},on:{click:function(t){return e.$router.replace(e.link)}}},[e.icon?n("q-item-section",{attrs:{avatar:""}},[n("q-icon",{attrs:{name:e.icon}})],1):e._e(),n("q-item-section",[n("q-item-label",[e._v(e._s(e.title))]),n("q-item-label",{attrs:{caption:""}},[e._v("\n "+e._s(e.caption)+"\n ")])],1)],1)},r=[],o={name:"EssentialLink",props:{title:{type:String,required:!0},caption:{type:String,default:""},link:{type:String,default:"#"},icon:{type:String,default:""}}},s=o,c=n("2877"),u=n("66e5"),p=n("4074"),b=n("0016"),f=n("0170"),k=n("eebe"),m=n.n(k),d=Object(c["a"])(s,i,r,!1,null,null,null),w=d.exports;m()(d,"components",{QItem:u["a"],QItemSection:p["a"],QIcon:b["a"],QItemLabel:f["a"]});const q=[{title:"首页",link:"/"},{title:"激活",link:"/activate"},{title:"卡密管理",link:"/regkey"},{title:"群组管理",link:"/group"}];var v={name:"MainLayout",components:{EssentialLink:w},data(){return{leftDrawerOpen:!1,essentialLinks:q}}},Q=v,_=n("4d5a"),g=n("e359"),L=n("65c6"),y=n("9c40"),h=n("6ac5"),O=n("9404"),D=n("1c1c"),E=n("09e3"),I=Object(c["a"])(Q,a,l,!1,null,null,null);t["default"]=I.exports;m()(I,"components",{QLayout:_["a"],QHeader:g["a"],QToolbar:L["a"],QBtn:y["a"],QToolbarTitle:h["a"],QDrawer:O["a"],QList:D["a"],QItemLabel:f["a"],QPageContainer:E["a"]})}}]); -------------------------------------------------------------------------------- /vue/js/5.69a84b0f.js: -------------------------------------------------------------------------------- 1 | (window["webpackJsonp"]=window["webpackJsonp"]||[]).push([[5],{"713b":function(e,t,n){"use strict";n.r(t);var a=function(){var e=this,t=e.$createElement,n=e._self._c||t;return n("q-layout",{attrs:{view:"lHh Lpr lFf"}},[n("q-header",{attrs:{elevated:""}},[n("q-toolbar",[n("q-btn",{attrs:{flat:"",dense:"",round:"",icon:"menu","aria-label":"Menu"},on:{click:function(t){e.leftDrawerOpen=!e.leftDrawerOpen}}}),n("q-toolbar-title",[e._v("\n Hoshino授权管理系统\n ")])],1)],1),n("q-drawer",{attrs:{"show-if-above":"",bordered:"","content-class":"bg-grey-1"},model:{value:e.leftDrawerOpen,callback:function(t){e.leftDrawerOpen=t},expression:"leftDrawerOpen"}},[n("q-list",[n("q-item-label",{staticClass:"text-grey-8",attrs:{header:""}},[e._v("\n 导航\n ")]),e._l(e.essentialLinks,(function(t){return n("EssentialLink",e._b({key:t.title},"EssentialLink",t,!1))}))],2)],1),n("q-page-container",[n("router-view")],1)],1)},l=[],i=function(){var e=this,t=e.$createElement,n=e._self._c||t;return n("q-item",{attrs:{clickable:"",tag:"a"},on:{click:function(t){return e.$router.replace(e.link)}}},[e.icon?n("q-item-section",{attrs:{avatar:""}},[n("q-icon",{attrs:{name:e.icon}})],1):e._e(),n("q-item-section",[n("q-item-label",[e._v(e._s(e.title))]),n("q-item-label",{attrs:{caption:""}},[e._v("\n "+e._s(e.caption)+"\n ")])],1)],1)},r=[],o={name:"EssentialLink",props:{title:{type:String,required:!0},caption:{type:String,default:""},link:{type:String,default:"#"},icon:{type:String,default:""}}},s=o,c=n("2877"),u=n("66e5"),p=n("4074"),b=n("0016"),f=n("0170"),k=n("eebe"),m=n.n(k),d=Object(c["a"])(s,i,r,!1,null,null,null),w=d.exports;m()(d,"components",{QItem:u["a"],QItemSection:p["a"],QIcon:b["a"],QItemLabel:f["a"]});const q=[{title:"首页",link:"/"},{title:"激活",link:"/activate"},{title:"卡密管理",link:"/regkey"},{title:"群组管理",link:"/group"}];var v={name:"MainLayout",components:{EssentialLink:w},data(){return{leftDrawerOpen:!1,essentialLinks:q}}},Q=v,_=n("4d5a"),g=n("e359"),L=n("65c6"),y=n("9c40"),h=n("6ac5"),O=n("9404"),D=n("1c1c"),E=n("09e3"),I=Object(c["a"])(Q,a,l,!1,null,null,null);t["default"]=I.exports;m()(I,"components",{QLayout:_["a"],QHeader:g["a"],QToolbar:L["a"],QBtn:y["a"],QToolbarTitle:h["a"],QDrawer:O["a"],QList:D["a"],QItemLabel:f["a"],QPageContainer:E["a"]})}}]); -------------------------------------------------------------------------------- /authMS.py.example: -------------------------------------------------------------------------------- 1 | # 注意, 此配置中的部分选项可能暂时不生效, 请以README中列举出的功能为准 2 | 3 | class auth_config(object): 4 | 5 | # 授权系统自动检测总开关, 默认为关闭状态,此时不会自动检测/自动退群/停止响应无授权群聊 6 | # 设置此项目的是初次使用authMS时系统时的过渡选项, 正式使用应当配置为True 7 | ENABLE_AUTH = False 8 | 9 | # ------------------网页管理--------------------- 10 | # 是否启用WEB 11 | ENABLE_WEB = False 12 | 13 | # WEB管理密码, 推荐使用强密码 14 | PASSWORD = 'Password123456!' 15 | 16 | 17 | # -------------------事件过滤器--------------------- 18 | # 事件过滤器配置文件的地址,例如/root/go-cqhttp/filter.json(必须填写) 19 | EVENT_FILTER = r'' 20 | 21 | 22 | # -------------------数据互通--------------------- 23 | # 互通数据库的目录,例如group.sqlite位置/root/database/group.sqlite 24 | # 此时便填写/root/database/即可 25 | DB_PATH = '' 26 | 27 | # 是否启用互通 28 | ENABLE_COM = False 29 | 30 | 31 | # --------------------充值/卡密相关---------------- 32 | 33 | # 是否允许私聊充值 34 | ALLOW_PRIVATE_REG = True 35 | 36 | 37 | #卡密开头, 可以是字母或数字, 卡密总长度固定为16位, 因此不推荐太长 38 | BEGIN = '' 39 | 40 | 41 | # --------------------群组管理相关--------------------- 42 | 43 | # 授权到期后是否自动退群 44 | AUTO_LEAVE = True 45 | 46 | # 新群试用天数, 0天意味着机器人将不接受未授权群的入群邀请, 即便 47 | # 不允许重复试用, 如果群已经在试用列表且试用过期中仍然会被拒绝 48 | NEW_GROUP_DAYS = 0 49 | 50 | # 到期前的多少天开始提醒, 设置为0时将不会提醒 51 | REMIND_BRFORE_EXPIRED = 2 52 | 53 | # 到期后多少天退群, 仅当配置AUTO_LEAVE为True是此项有效, 设置为0则立即退群 54 | LEAVE_AFTER_DAYS = 0 55 | 56 | # 允许的最大群人数, 3000为不限制, 因为QQ群最大人数为3000 57 | # 进群后, 立刻检测一次人数(因为进群邀请不包含人数信息) 58 | MAX_GROUP_NUM = 70 59 | 60 | # 提醒/检查的时间间隔, 单位小时, 61 | # 值为1, 意为每小时检查一次, 注意实际每天的检查次数会向下取整 62 | FREQUENCY = 6 63 | 64 | # ----------------------授权列表与卡密列表---------------------------- 65 | 66 | # 私聊使用授权列表时, 每页显示的群的个数,不推荐超过10, 否则消息会非常长 67 | GROUPS_IN_PAGE = 5 68 | 69 | # 私聊查看卡密列表时, 每页显示的卡密的个数,不推荐超过20 70 | CARDS_IN_PAGE = 10 71 | 72 | #----------------------发言文本--------------------------- 73 | 74 | # 加入新群时的发言, 同时适用于50人以上群和50人以下群 75 | NEW_GROUP_MSG = ''' 76 | Bot已成功加入本群, 发送帮助以获得更多信息 77 | '''.strip() 78 | 79 | # 离开群之前的发言 80 | GROUP_LEAVE_MSG = ''' 81 | Bot即将退出本群 82 | 退群原因: 83 | '''.strip() 84 | 85 | 86 | # 提醒授权即将过期的发言, 在支持变量替换前不生效 87 | REMIND_MSG = ''' 88 | 本群授权即将到期 89 | '''.strip() 90 | 91 | REG_HELP_GROUP = ''' 92 | 实际使用时, 应当将abcd替换为您收到的16位卡密,12345替换为您的群号 93 | 授权系统指令示例, 发送方括号内的内容即可, 注意空格不可省略 94 | 群聊指令: 95 | [充值 abcd]为当前群充值 96 | [查询授权]查询当前群的授权状态 97 | [检查卡密 abcd]检查此卡密是否有效 98 | 私聊指令请私聊Bot发送[充值帮助] 99 | '''.strip() 100 | REG_HELP_PRIVATE = ''' 101 | 实际使用时, 应当将abcd替换为您收到的16位卡密,12345替换为您的群号 102 | 授权系统指令示例, 发送方括号内的内容即可, 注意空格不可省略 103 | 私聊指令: 104 | [充值 abcd*12345]为指定群充值 105 | [查询授权 12345]查询指定群的授权信息 106 | [检查卡密 abcd]检查此卡密是否有效 107 | 群聊指令请在群聊中发送[充值帮助] 108 | '''.strip() 109 | 110 | ADMIN_HELP=''' 111 | 实际使用时, 应当将abcd替换为16位卡密,12345替换为群号 112 | 超级管理员可用指令一览: 113 | [生成卡密 a*b]生成b张a天的卡密 114 | [卡密列表]查看卡密列表 115 | [授权列表]检查所有已授权群 116 | [变更授权 12345+1]为一个群变更授权时间, +或- 117 | [转移授权 12345*54321]转移已有群的授权 118 | [授权状态]查看Bot当前已加入群/已授权群的统计 119 | [清除授权 12345]完全移除一个群的授权并退群(如果配置了的话) 120 | [退群 12345]字面意思 121 | [变更所有授权 3]为所有已授权的群增加3天时间 122 | '''.strip() 123 | 124 | # --------------------加好友相关------------------------------- 125 | # 自动接受加好友请求, 请把加我为好友的方式设置为“需要验证信息” 126 | FRIEND_APPROVE = True 127 | 128 | # ------------------------日志----------------------------- 129 | # 日志, 记录卡密生成, 卡密使用, 群组加入, 群组退出, 群组过期, 好友添加 130 | LOG = True 131 | 132 | # 调试开关, 记录本系统所有信息 133 | DEBUG = False 134 | 135 | -------------------------------------------------------------------------------- /vue/js/2.c47ca998.js: -------------------------------------------------------------------------------- 1 | (window["webpackJsonp"]=window["webpackJsonp"]||[]).push([[2],{"25b6":function(t,e,a){"use strict";var s=a("862c"),i=a.n(s);i.a},"862c":function(t,e,a){},ad61:function(t,e,a){"use strict";a.r(e);var s=function(){var t=this,e=t.$createElement,a=t._self._c||e;return a("q-page",{staticClass:"doc-page"},[a("div",{staticClass:"q-pa-md"},[a("q-table",{attrs:{title:"Treats",data:t.data,columns:t.columns,"row-key":"key",selection:"multiple",selected:t.selected},on:{"update:selected":function(e){t.selected=e}},scopedSlots:t._u([{key:"top",fn:function(){return[a("div",{staticClass:"q-table__title row"},[t._v("\n 充值卡密管理\n ")]),a("q-space"),a("div",{staticClass:"row"},[a("q-btn",{staticClass:"q-mr-xs",attrs:{color:"positive"},on:{click:t.output}},[t._v("导出")]),a("q-btn",{staticClass:"q-mr-xs",attrs:{color:"info"},on:{click:t.updateKey}},[t._v("刷新")]),a("q-btn",{staticClass:"q-mr-xs",attrs:{color:"secondary"},on:{click:function(e){t.isAdd=!0}}},[t._v("添加")]),a("q-btn",{staticClass:"q-mr-xs",attrs:{color:"primary"},on:{click:function(e){return t.modifySelect()}}},[t._v("修改")]),a("q-btn",{staticClass:"q-mr-xs",attrs:{color:"negative"},on:{click:t.deleteSelect}},[t._v("删除")])],1)]},proxy:!0}])})],1),a("q-dialog",{model:{value:t.isAdd,callback:function(e){t.isAdd=e},expression:"isAdd"}},[a("q-card",{staticClass:"flex flex-center"},[a("div",{staticClass:"q-pa-md",staticStyle:{width:"480px"}},[a("q-form",{staticClass:"q-gutter-md"},[a("q-input",{attrs:{filled:"",type:"number",label:"时长","lazy-rules":"",rules:[function(t){return null!==t&&t>0},"请正确的时长!"]},model:{value:t.duration,callback:function(e){t.duration=e},expression:"duration"}}),a("q-input",{attrs:{filled:"",type:"number",label:"数量","lazy-rules":"",rules:[function(t){return null!==t&&t>0},"请正确的数量!"]},model:{value:t.num,callback:function(e){t.num=e},expression:"num"}}),a("div",{attrs:{align:"center"}},[a("q-btn",{attrs:{label:"Submit",color:"primary"},on:{click:function(e){return t.onSubmit()}}})],1)],1)],1)])],1)],1)},i=[],l=a("18d6"),n={data(){return{selected:[],columns:[{name:"key",required:!0,label:"充值卡密",align:"left",field:t=>t.key,format:t=>""+t,sortable:!0},{name:"duration",align:"right",label:"时长",field:"duration",sortable:!0,sort:(t,e)=>parseInt(t,10)-parseInt(e,10)}],data:[{key:"error",duration:114},{key:"请清理浏览器缓存",duration:514}],isAdd:!1,num:1,duration:30}},methods:{updateKey(){this.$axios.get("/get/key?password="+l["a"].getItem("password")).then(t=>{this.data=t.data})},deleteSelect(){this.$q.dialog({title:"Confirm",message:"此操作将永久删除, 是否继续?",cancel:!0,persistent:!0}).onOk(()=>{}).onOk(()=>{this.selected.forEach(t=>{this.deleteItem(t)}),this.updateKey(),this.message("成功","删除完毕!")})},deleteItem(t){this.$axios.delete("del/key?key="+t.key)},modifySelect(){this.$q.dialog({title:"修改",message:"请填写注册码的授权时长",prompt:{model:"",isValid:t=>t.length>0,type:"number"},cancel:!0,persistent:!0}).onOk(t=>{this.selected.forEach(e=>{this.modifyItem(e,t)}),this.updateKey(),this.message("成功","修改完毕!")})},modifyItem(t,e){this.$axios.post("update/key?key="+t.key+"&duration="+e)},message(t,e){this.$q.dialog({title:t,message:e,html:!0})},output(){let t="";this.selected.forEach(e=>{t=t+"卡密:"+e.key+"
时长:"+e.duration+"天
"}),this.message("导出卡密",t)},onSubmit(){this.$axios.post("add/key?duration="+this.duration+"&num="+this.num).then(t=>{t&&(this.message("成功","添加完成!"),this.isAdd=!1,this.updateKey())}).catch(t=>{console.log(t)})}},created(){this.updateKey()}},o=n,r=(a("25b6"),a("2877")),c=a("9989"),d=a("eaac"),u=a("2c91"),m=a("9c40"),p=a("24e8"),f=a("f09f"),h=a("0378"),y=a("27f9"),b=a("eebe"),k=a.n(b),g=Object(r["a"])(o,s,i,!1,null,null,null);e["default"]=g.exports;k()(g,"components",{QPage:c["a"],QTable:d["a"],QSpace:u["a"],QBtn:m["a"],QDialog:p["a"],QCard:f["a"],QForm:h["a"],QInput:y["a"]})}}]); -------------------------------------------------------------------------------- /vue/js/3.39f6dfb4.js: -------------------------------------------------------------------------------- 1 | (window["webpackJsonp"]=window["webpackJsonp"]||[]).push([[3],{"25b6":function(e,t,s){"use strict";var a=s("862c"),i=s.n(a);i.a},"862c":function(e,t,s){},ad61:function(e,t,s){"use strict";s.r(t);var a=function(){var e=this,t=e.$createElement,s=e._self._c||t;return s("q-page",{staticClass:"doc-page"},[s("div",{staticClass:"q-pa-md"},[s("q-table",{attrs:{title:"Treats",data:e.data,columns:e.columns,"row-key":"key",selection:"multiple",selected:e.selected,filter:e.filter},on:{"update:selected":function(t){e.selected=t}},scopedSlots:e._u([{key:"top",fn:function(){return[s("div",{staticClass:"q-table__title row"},[e._v("\n 充值卡密管理\n ")]),s("q-space"),s("div",{staticClass:"row"},[s("q-input",{attrs:{borderless:"",dense:"",debounce:"300",placeholder:"Search"},scopedSlots:e._u([{key:"append",fn:function(){return[s("q-icon",{attrs:{name:"search"}})]},proxy:!0}]),model:{value:e.filter,callback:function(t){e.filter=t},expression:"filter"}}),s("q-btn",{staticClass:"q-mr-xs",attrs:{color:"positive"},on:{click:e.output}},[e._v("导出")]),s("q-btn",{staticClass:"q-mr-xs",attrs:{color:"info"},on:{click:e.updateKey}},[e._v("刷新")]),s("q-btn",{staticClass:"q-mr-xs",attrs:{color:"secondary"},on:{click:function(t){e.isAdd=!0}}},[e._v("添加")]),s("q-btn",{staticClass:"q-mr-xs",attrs:{color:"primary"},on:{click:function(t){return e.modifySelect()}}},[e._v("修改")]),s("q-btn",{staticClass:"q-mr-xs",attrs:{color:"negative"},on:{click:e.deleteSelect}},[e._v("删除")])],1)]},proxy:!0}])})],1),s("q-dialog",{model:{value:e.isAdd,callback:function(t){e.isAdd=t},expression:"isAdd"}},[s("q-card",{staticClass:"flex flex-center"},[s("div",{staticClass:"q-pa-md",staticStyle:{width:"480px"}},[s("q-form",{staticClass:"q-gutter-md"},[s("q-input",{attrs:{filled:"",type:"number",label:"时长","lazy-rules":"",rules:[function(e){return null!==e&&e>0},"请正确的时长!"]},model:{value:e.duration,callback:function(t){e.duration=t},expression:"duration"}}),s("q-input",{attrs:{filled:"",type:"number",label:"数量","lazy-rules":"",rules:[function(e){return null!==e&&e>0},"请正确的数量!"]},model:{value:e.num,callback:function(t){e.num=t},expression:"num"}}),s("div",{attrs:{align:"center"}},[s("q-btn",{attrs:{label:"Submit",color:"primary"},on:{click:function(t){return e.onSubmit()}}})],1)],1)],1)])],1)],1)},i=[],l=s("18d6"),n={data(){return{filter:"",selected:[],columns:[{name:"key",required:!0,label:"充值卡密",align:"left",field:e=>e.key,format:e=>""+e,sortable:!0},{name:"duration",align:"right",label:"时长",field:"duration",sortable:!0,sort:(e,t)=>parseInt(e,10)-parseInt(t,10)}],data:[],isAdd:!1,num:1,duration:30}},methods:{updateKey(){this.$axios.get("/get/key?password="+l["a"].getItem("password")).then(e=>{this.data=e.data})},deleteSelect(){this.$q.dialog({title:"Confirm",message:"此操作将永久删除, 是否继续?",cancel:!0,persistent:!0}).onOk(()=>{}).onOk(()=>{this.selected.forEach(e=>{this.deleteItem(e)}),this.updateKey(),this.message("成功","删除完毕!")})},deleteItem(e){this.$axios.delete("del/key?key="+e.key)},modifySelect(){this.$q.dialog({title:"修改",message:"请填写注册码的授权时长",prompt:{model:"",isValid:e=>e.length>0,type:"number"},cancel:!0,persistent:!0}).onOk(e=>{this.selected.forEach(t=>{this.modifyItem(t,e)}),this.updateKey(),this.message("成功","修改完毕!")})},modifyItem(e,t){this.$axios.post("update/key?key="+e.key+"&duration="+t)},message(e,t){this.$q.dialog({title:e,message:t,html:!0})},output(){let e="";this.selected.forEach(t=>{e=e+"卡密:"+t.key+"
时长:"+t.duration+"天
"}),this.message("导出卡密",e)},onSubmit(){this.$axios.post("add/key?duration="+this.duration+"&num="+this.num).then(e=>{e&&(this.message("成功","添加完成!"),this.isAdd=!1,this.updateKey())}).catch(e=>{console.log(e)})}},created(){this.updateKey()}},o=n,r=(s("25b6"),s("2877")),c=s("9989"),d=s("eaac"),u=s("2c91"),m=s("27f9"),p=s("0016"),f=s("9c40"),h=s("24e8"),b=s("f09f"),y=s("0378"),k=s("eebe"),q=s.n(k),g=Object(r["a"])(o,a,i,!1,null,null,null);t["default"]=g.exports;q()(g,"components",{QPage:c["a"],QTable:d["a"],QSpace:u["a"],QInput:m["a"],QIcon:p["a"],QBtn:f["a"],QDialog:h["a"],QCard:b["a"],QForm:y["a"]})}}]); -------------------------------------------------------------------------------- /vue/js/3.0457a8b4.js: -------------------------------------------------------------------------------- 1 | (window["webpackJsonp"]=window["webpackJsonp"]||[]).push([[3],{"25b6":function(e,t,s){"use strict";var a=s("862c"),i=s.n(a);i.a},"862c":function(e,t,s){},ad61:function(e,t,s){"use strict";s.r(t);var a=function(){var e=this,t=e.$createElement,s=e._self._c||t;return s("q-page",{staticClass:"doc-page"},[s("div",{staticClass:"q-pa-md"},[s("q-table",{attrs:{title:"Treats",data:e.data,columns:e.columns,"row-key":"key",selection:"multiple",selected:e.selected,filter:e.filter},on:{"update:selected":function(t){e.selected=t}},scopedSlots:e._u([{key:"top",fn:function(){return[s("div",{staticClass:"q-table__title row"},[e._v("\n 充值卡密管理\n ")]),s("q-space"),s("div",{staticClass:"row"},[s("q-input",{attrs:{borderless:"",dense:"",debounce:"300",placeholder:"Search"},scopedSlots:e._u([{key:"append",fn:function(){return[s("q-icon",{attrs:{name:"search"}})]},proxy:!0}]),model:{value:e.filter,callback:function(t){e.filter=t},expression:"filter"}}),s("q-btn",{staticClass:"q-mr-xs",attrs:{color:"positive"},on:{click:e.output}},[e._v("导出")]),s("q-btn",{staticClass:"q-mr-xs",attrs:{color:"info"},on:{click:e.updateKey}},[e._v("刷新")]),s("q-btn",{staticClass:"q-mr-xs",attrs:{color:"secondary"},on:{click:function(t){e.isAdd=!0}}},[e._v("添加")]),s("q-btn",{staticClass:"q-mr-xs",attrs:{color:"primary"},on:{click:function(t){return e.modifySelect()}}},[e._v("修改")]),s("q-btn",{staticClass:"q-mr-xs",attrs:{color:"negative"},on:{click:e.deleteSelect}},[e._v("删除")])],1)]},proxy:!0}])})],1),s("q-dialog",{model:{value:e.isAdd,callback:function(t){e.isAdd=t},expression:"isAdd"}},[s("q-card",{staticClass:"flex flex-center"},[s("div",{staticClass:"q-pa-md",staticStyle:{width:"480px"}},[s("q-form",{staticClass:"q-gutter-md"},[s("q-input",{attrs:{filled:"",type:"number",label:"时长","lazy-rules":"",rules:[function(e){return null!==e&&e>0},"请正确的时长!"]},model:{value:e.duration,callback:function(t){e.duration=t},expression:"duration"}}),s("q-input",{attrs:{filled:"",type:"number",label:"数量","lazy-rules":"",rules:[function(e){return null!==e&&e>0},"请正确的数量!"]},model:{value:e.num,callback:function(t){e.num=t},expression:"num"}}),s("div",{attrs:{align:"center"}},[s("q-btn",{attrs:{label:"Submit",color:"primary"},on:{click:function(t){return e.onSubmit()}}})],1)],1)],1)])],1)],1)},i=[],l=s("18d6"),n={data(){return{filter:"",selected:[],columns:[{name:"key",required:!0,label:"充值卡密",align:"left",field:e=>e.key,format:e=>""+e,sortable:!0},{name:"duration",align:"right",label:"时长",field:"duration",sortable:!0,sort:(e,t)=>parseInt(e,10)-parseInt(t,10)}],data:[],isAdd:!1,num:1,duration:30}},methods:{updateKey(){this.$axios.get("/get/key?password="+l["a"].getItem("password")).then(e=>{this.data=e.data})},deleteSelect(){this.$q.dialog({title:"Confirm",message:"此操作将永久删除, 是否继续?",cancel:!0,persistent:!0}).onOk(()=>{}).onOk(()=>{this.selected.forEach(e=>{this.deleteItem(e)}),this.updateKey(),this.message("成功","删除完毕!")})},deleteItem(e){this.$axios.delete("del/key?key="+e.key+"&password="+l["a"].getItem("password"))},modifySelect(){this.$q.dialog({title:"修改",message:"请填写注册码的授权时长",prompt:{model:"",isValid:e=>e.length>0,type:"number"},cancel:!0,persistent:!0}).onOk(e=>{this.selected.forEach(t=>{this.modifyItem(t,e)}),this.updateKey(),this.message("成功","修改完毕!")})},modifyItem(e,t){this.$axios.post("update/key?key="+e.key+"&duration="+t+"&password="+l["a"].getItem("password"))},message(e,t){this.$q.dialog({title:e,message:t,html:!0})},output(){let e="";this.selected.forEach(t=>{e=e+"卡密:"+t.key+"
时长:"+t.duration+"天
"}),this.message("导出卡密",e)},onSubmit(){this.$axios.post("add/key?duration="+this.duration+"&num="+this.num+"&password="+l["a"].getItem("password")).then(e=>{e&&(this.message("成功","添加完成!"),this.isAdd=!1,this.updateKey())}).catch(e=>{console.log(e)})}},created(){this.updateKey()}},o=n,r=(s("25b6"),s("2877")),c=s("9989"),d=s("eaac"),u=s("2c91"),m=s("27f9"),p=s("0016"),f=s("9c40"),h=s("24e8"),b=s("f09f"),y=s("0378"),g=s("eebe"),k=s.n(g),q=Object(r["a"])(o,a,i,!1,null,null,null);t["default"]=q.exports;k()(q,"components",{QPage:c["a"],QTable:d["a"],QSpace:u["a"],QInput:m["a"],QIcon:p["a"],QBtn:f["a"],QDialog:h["a"],QCard:b["a"],QForm:y["a"]})}}]); -------------------------------------------------------------------------------- /web_server.py: -------------------------------------------------------------------------------- 1 | from hoshino import Service, priv 2 | from quart import request, Blueprint, jsonify, render_template 3 | 4 | import string 5 | import random 6 | import nonebot 7 | import hoshino 8 | 9 | from datetime import * 10 | from . import util 11 | from .constant import config 12 | 13 | 14 | auth = Blueprint('auth', __name__, url_prefix='/auth', template_folder="./vue", static_folder='./vue', 15 | static_url_path='') 16 | bot = nonebot.get_bot() 17 | app = bot.server_app 18 | manage_password = config.PASSWORD # 管理密码请在authMS.py中修改 19 | 20 | 21 | @auth.route('/') 22 | async def index(): 23 | return await render_template("index.html") 24 | 25 | 26 | @auth.route('/api/login', methods=['POST']) 27 | async def login_auth(): 28 | password = request.args.get('password') 29 | if password == manage_password: 30 | return 'success' 31 | return 'failed' 32 | 33 | 34 | @auth.route('/api/get/key', methods=['GET']) 35 | async def get_key(): 36 | password = request.args.get('password') 37 | if password != manage_password: 38 | return 'failed' 39 | return jsonify(util.get_key_list()) 40 | 41 | 42 | @auth.route('/api/add/key', methods=['POST']) 43 | async def add_key(): 44 | password = request.args.get('password') 45 | if password != manage_password: 46 | return 'failed' 47 | if request.method == 'POST': 48 | duration = int(request.args.get('duration')) 49 | num = int(request.args.get('num')) 50 | for _ in range(num): 51 | util.add_key(duration) 52 | return 'success' 53 | return 'failed' 54 | 55 | 56 | @auth.route('/api/del/key', methods=['DELETE']) 57 | async def del_key(): 58 | password = request.args.get('password') 59 | if password != manage_password: 60 | return 'failed' 61 | if request.method == 'DELETE': 62 | key = request.args.get('key') 63 | return jsonify(util.del_key(key)) 64 | 65 | 66 | @auth.route('/api/update/key', methods=['POST']) 67 | async def update_key(): 68 | password = request.args.get('password') 69 | if password != manage_password: 70 | return 'failed' 71 | key = request.args.get('key') 72 | duration = int(request.args.get('duration')) 73 | return jsonify(util.update_key(key, duration)) 74 | 75 | 76 | @auth.route('/api/get/group', methods=['GET']) 77 | async def get_group(): 78 | password = request.args.get('password') 79 | if password != manage_password: 80 | return 'failed' 81 | return jsonify(await util.get_authed_group_list()) 82 | 83 | 84 | @auth.route('/api/add/group', methods=['POST']) 85 | @auth.route('/api/update/group', methods=['POST']) 86 | async def update_group(): 87 | password = request.args.get('password') 88 | if password != manage_password: 89 | return 'failed' 90 | gid = int(request.args.get('gid')) 91 | time_change = int(request.args.get('duration')) 92 | await util.change_authed_time(gid, time_change) 93 | return 'success' 94 | 95 | 96 | @auth.route('/api/activate', methods=['POST']) 97 | async def activate_group(): 98 | key = request.args.get('key') 99 | gid = request.args.get('gid') 100 | if await util.reg_group(gid, key): 101 | return 'success' 102 | return 'failed' 103 | 104 | 105 | @auth.route('/api/notify/group', methods=['POST']) 106 | async def notify_group(): 107 | password = request.args.get('password') 108 | if password != manage_password: 109 | return 'failed' 110 | gid = int(request.args.get('gid')) 111 | msg = request.args.get('msg') 112 | if await util.notify_group(gid, msg): 113 | return 'success' 114 | return 'failed' 115 | 116 | 117 | @auth.route('/api/gun/group', methods=['POST']) 118 | async def gun_group(): 119 | password = request.args.get('password') 120 | if password != manage_password: 121 | return 'failed' 122 | gid = int(request.args.get('gid')) 123 | if await util.gun_group(gid): 124 | return 'success' 125 | return 'failed' 126 | -------------------------------------------------------------------------------- /vue/js/2.59ffaecf.js: -------------------------------------------------------------------------------- 1 | (window["webpackJsonp"]=window["webpackJsonp"]||[]).push([[2],{"2db1":function(t,e,a){"use strict";var s=a("39bc"),i=a.n(s);i.a},"39bc":function(t,e,a){},d684:function(t,e,a){"use strict";a.r(e);var s=function(){var t=this,e=t.$createElement,a=t._self._c||e;return a("q-page",{staticClass:"doc-page"},[a("div",{staticClass:"q-pa-md"},[a("q-table",{attrs:{title:"Treats",data:t.data,columns:t.columns,"row-key":"gid",selection:"multiple",selected:t.selected,filter:t.filter},on:{"update:selected":function(e){t.selected=e}},scopedSlots:t._u([{key:"top",fn:function(){return[a("div",{staticClass:"q-table__title row"},[t._v("\n 授权群组管理\n ")]),a("q-space"),a("div",{staticClass:"row"},[a("q-input",{attrs:{borderless:"",dense:"",debounce:"300",placeholder:"Search"},scopedSlots:t._u([{key:"append",fn:function(){return[a("q-icon",{attrs:{name:"search"}})]},proxy:!0}]),model:{value:t.filter,callback:function(e){t.filter=e},expression:"filter"}}),a("q-btn",{staticClass:"q-mr-xs",attrs:{color:"info"},on:{click:t.updateGroup}},[t._v("刷新")]),a("q-btn",{staticClass:"q-mr-xs",attrs:{color:"secondary"},on:{click:function(e){return t.modifySelect()}}},[t._v("修改")]),a("q-btn",{staticClass:"q-mr-xs",attrs:{color:"primary"},on:{click:function(e){t.isAdd=!0}}},[t._v("添加授权")]),a("q-btn",{staticClass:"q-mr-xs",attrs:{color:"accent"},on:{click:t.broadcastSelect}},[t._v("提醒续费")]),a("q-btn",{staticClass:"q-mr-xs",attrs:{color:"negative"},on:{click:t.gunSelect}},[t._v("退群")])],1)]},proxy:!0}])})],1),a("q-dialog",{model:{value:t.isAdd,callback:function(e){t.isAdd=e},expression:"isAdd"}},[a("q-card",{staticClass:"flex flex-center"},[a("div",{staticClass:"q-pa-md",staticStyle:{width:"480px"}},[a("q-form",{staticClass:"q-gutter-md"},[a("q-input",{attrs:{filled:"",type:"number",label:"群号","lazy-rules":"",rules:[function(t){return null!==t&&t>0},"请正确的群号!"]},model:{value:t.addGid,callback:function(e){t.addGid=e},expression:"addGid"}}),a("q-input",{attrs:{filled:"",type:"number",label:"时长","lazy-rules":"",rules:[function(t){return null!==t&&t>0},"请正确的时长!"]},model:{value:t.duration,callback:function(e){t.duration=e},expression:"duration"}}),a("div",{attrs:{align:"center"}},[a("q-btn",{attrs:{label:"Submit",color:"primary"},on:{click:function(e){return t.onSubmit()}}})],1)],1)],1)])],1)],1)},i=[],o=a("18d6"),l={data(){return{filter:"",selected:[],columns:[{name:"gid",required:!0,label:"群号",align:"left",field:t=>t.gid,format:t=>""+t,sortable:!0,sort:(t,e)=>parseInt(t,20)-parseInt(e,20)},{name:"groupName",required:!0,label:"群名称",align:"left",field:t=>t.groupName,format:t=>""+t},{name:"deadline",align:"right",label:"截至日期",field:"deadline",sortable:!0}],data:[],isAdd:!1,addGid:123456,duration:30}},methods:{updateGroup(){this.$axios.get("/get/group?password="+o["a"].getItem("password")).then(t=>{this.data=t.data,console.log(t.data)})},modifySelect(){this.$q.dialog({title:"修改",message:"请填写群组增加(减少)授权时长",prompt:{model:"",isValid:t=>t.length>0,type:"number"},cancel:!0,persistent:!0}).onOk(t=>{this.selected.forEach(e=>{this.modifyItem(e,t)}),this.updateGroup(),this.message("成功","修改完毕!")})},modifyItem(t,e){this.$axios.post("update/group?gid="+t.gid+"&duration="+e)},message(t,e){this.$q.dialog({title:t,message:e,html:!0})},onSubmit(){this.$axios.post("add/group?gid="+this.addGid+"&duration="+this.duration).then(t=>{t&&(this.message("成功","添加完成!"),this.isAdd=!1,this.updateGroup())}).catch(t=>{console.log(t)})},broadcastSelect(){this.$q.dialog({title:"请填写推送语",prompt:{model:"您的机器人即将到期,请及时续费!",type:"text"},cancel:!0}).onOk(t=>{this.selected.forEach(e=>{this.broadcastItem(e,t)})})},broadcastItem(t,e){this.$axios.post("/notify/group?gid="+t.gid+"&msg="+e).then(e=>{"failed"===e.data?this.$q.notify({message:"群"+t.gid+"推送失败!",position:"top",color:"purple"}):this.$q.notify({message:"群"+t.gid+"推送成功!",position:"top",color:"green"})}).catch(()=>{this.$q.notify({message:"群"+t.gid+"推送失败!",position:"top",color:"purple"})})},gunSelect(){this.$q.dialog({title:"确认",message:"确定退出群聊吗?",cancel:!0}).onOk(t=>{this.selected.forEach(t=>{this.gunItem(t)}),this.updateGroup()})},gunItem(t){this.$axios.post("/gun/group?gid="+t.gid).then(e=>{"failed"===e.data?this.$q.notify({message:"群"+t.gid+"退出失败!",position:"top",color:"purple"}):this.$q.notify({message:"群"+t.gid+"退出成功!",position:"top",color:"green"})}).catch(()=>{this.$q.notify({message:"群"+t.gid+"推送失败!",position:"top",color:"purple"})})}},created(){this.updateGroup()}},n=l,r=(a("2db1"),a("2877")),d=a("9989"),c=a("eaac"),u=a("2c91"),p=a("27f9"),m=a("0016"),g=a("9c40"),f=a("24e8"),h=a("f09f"),b=a("0378"),q=a("eebe"),y=a.n(q),x=Object(r["a"])(n,s,i,!1,null,null,null);e["default"]=x.exports;y()(x,"components",{QPage:d["a"],QTable:c["a"],QSpace:u["a"],QInput:p["a"],QIcon:m["a"],QBtn:g["a"],QDialog:f["a"],QCard:h["a"],QForm:b["a"]})}}]); -------------------------------------------------------------------------------- /vue/js/2.4383789c.js: -------------------------------------------------------------------------------- 1 | (window["webpackJsonp"]=window["webpackJsonp"]||[]).push([[2],{"2db1":function(t,e,a){"use strict";var s=a("39bc"),i=a.n(s);i.a},"39bc":function(t,e,a){},d684:function(t,e,a){"use strict";a.r(e);var s=function(){var t=this,e=t.$createElement,a=t._self._c||e;return a("q-page",{staticClass:"doc-page"},[a("div",{staticClass:"q-pa-md"},[a("q-table",{attrs:{title:"Treats",data:t.data,columns:t.columns,"row-key":"gid",selection:"multiple",selected:t.selected,filter:t.filter},on:{"update:selected":function(e){t.selected=e}},scopedSlots:t._u([{key:"top",fn:function(){return[a("div",{staticClass:"q-table__title row"},[t._v("\n 授权群组管理\n ")]),a("q-space"),a("div",{staticClass:"row"},[a("q-input",{attrs:{borderless:"",dense:"",debounce:"300",placeholder:"Search"},scopedSlots:t._u([{key:"append",fn:function(){return[a("q-icon",{attrs:{name:"search"}})]},proxy:!0}]),model:{value:t.filter,callback:function(e){t.filter=e},expression:"filter"}}),a("q-btn",{staticClass:"q-mr-xs",attrs:{color:"info"},on:{click:t.updateGroup}},[t._v("刷新")]),a("q-btn",{staticClass:"q-mr-xs",attrs:{color:"secondary"},on:{click:function(e){return t.modifySelect()}}},[t._v("修改")]),a("q-btn",{staticClass:"q-mr-xs",attrs:{color:"primary"},on:{click:function(e){t.isAdd=!0}}},[t._v("添加授权")]),a("q-btn",{staticClass:"q-mr-xs",attrs:{color:"accent"},on:{click:t.broadcastSelect}},[t._v("提醒续费")]),a("q-btn",{staticClass:"q-mr-xs",attrs:{color:"negative"},on:{click:t.gunSelect}},[t._v("退群")])],1)]},proxy:!0}])})],1),a("q-dialog",{model:{value:t.isAdd,callback:function(e){t.isAdd=e},expression:"isAdd"}},[a("q-card",{staticClass:"flex flex-center"},[a("div",{staticClass:"q-pa-md",staticStyle:{width:"480px"}},[a("q-form",{staticClass:"q-gutter-md"},[a("q-input",{attrs:{filled:"",type:"number",label:"群号","lazy-rules":"",rules:[function(t){return null!==t&&t>0},"请正确的群号!"]},model:{value:t.addGid,callback:function(e){t.addGid=e},expression:"addGid"}}),a("q-input",{attrs:{filled:"",type:"number",label:"时长","lazy-rules":"",rules:[function(t){return null!==t&&t>0},"请正确的时长!"]},model:{value:t.duration,callback:function(e){t.duration=e},expression:"duration"}}),a("div",{attrs:{align:"center"}},[a("q-btn",{attrs:{label:"Submit",color:"primary"},on:{click:function(e){return t.onSubmit()}}})],1)],1)],1)])],1)],1)},i=[],o=a("18d6"),l={data(){return{filter:"",selected:[],columns:[{name:"gid",required:!0,label:"群号",align:"left",field:t=>t.gid,format:t=>""+t,sortable:!0,sort:(t,e)=>parseInt(t,20)-parseInt(e,20)},{name:"groupName",required:!0,label:"群名称",align:"left",field:t=>t.groupName,format:t=>""+t},{name:"deadline",align:"right",label:"截至日期",field:"deadline",sortable:!0}],data:[],isAdd:!1,addGid:123456,duration:30}},methods:{updateGroup(){this.$axios.get("/get/group?password="+o["a"].getItem("password")).then(t=>{this.data=t.data,console.log(t.data)})},modifySelect(){this.$q.dialog({title:"修改",message:"请填写群组增加(减少)授权时长",prompt:{model:"",isValid:t=>t.length>0,type:"number"},cancel:!0,persistent:!0}).onOk(t=>{this.selected.forEach(e=>{this.modifyItem(e,t)}),this.updateGroup(),this.message("成功","修改完毕!")})},modifyItem(t,e){this.$axios.post("update/group?gid="+t.gid+"&duration="+e+"&password="+o["a"].getItem("password"))},message(t,e){this.$q.dialog({title:t,message:e,html:!0})},onSubmit(){this.$axios.post("add/group?gid="+this.addGid+"&duration="+this.duration+"&password="+o["a"].getItem("password")).then(t=>{t&&(this.message("成功","添加完成!"),this.isAdd=!1,this.updateGroup())}).catch(t=>{console.log(t)})},broadcastSelect(){this.$q.dialog({title:"请填写推送语",prompt:{model:"您的机器人即将到期,请及时续费!",type:"text"},cancel:!0}).onOk(t=>{this.selected.forEach(e=>{this.broadcastItem(e,t)})})},broadcastItem(t,e){this.$axios.post("/notify/group?gid="+t.gid+"&msg="+e+"&password="+o["a"].getItem("password")).then(e=>{"failed"===e.data?this.$q.notify({message:"群"+t.gid+"推送失败!",position:"top",color:"purple"}):this.$q.notify({message:"群"+t.gid+"推送成功!",position:"top",color:"green"})}).catch(()=>{this.$q.notify({message:"群"+t.gid+"推送失败!",position:"top",color:"purple"})})},gunSelect(){this.$q.dialog({title:"确认",message:"确定退出群聊吗?",cancel:!0}).onOk(t=>{this.selected.forEach(t=>{this.gunItem(t)}),this.updateGroup()})},gunItem(t){this.$axios.post("/gun/group?gid="+t.gid+"&password="+o["a"].getItem("password")).then(e=>{"failed"===e.data?this.$q.notify({message:"群"+t.gid+"退出失败!",position:"top",color:"purple"}):this.$q.notify({message:"群"+t.gid+"退出成功!",position:"top",color:"green"})}).catch(()=>{this.$q.notify({message:"群"+t.gid+"推送失败!",position:"top",color:"purple"})})}},created(){this.updateGroup()}},n=l,r=(a("2db1"),a("2877")),d=a("9989"),c=a("eaac"),p=a("2c91"),u=a("27f9"),m=a("0016"),g=a("9c40"),f=a("24e8"),h=a("f09f"),b=a("0378"),q=a("eebe"),y=a.n(q),x=Object(r["a"])(n,s,i,!1,null,null,null);e["default"]=x.exports;y()(x,"components",{QPage:d["a"],QTable:c["a"],QSpace:p["a"],QInput:u["a"],QIcon:m["a"],QBtn:g["a"],QDialog:f["a"],QCard:h["a"],QForm:b["a"]})}}]); -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # authMS 2 | 3 | 适用于HoshinoBot v2的授权插件, 可控制cqhttp的事件过滤器. 大部分功能以天枢授权为蓝本而开发. 本插件另有带有web服务的页面, 如果需要请按照注释开启web. 本项目主体框架由[wdvxdr1123](https://github.com/wdvxdr1123)构建, [火龙](https://github.com/xhl6666)添加了一些重要功能. 4 | 5 | 6 | 本授权系统的开发调试过程均以[go-cqhttp](https://github.com/Mrs4s/go-cqhttp)进行, 请使用v0.9.24-fix2以上版本的[go-cqhttp](https://github.com/Mrs4s/go-cqhttp); 不支持v1.0.0以上版本的[go-cqhttp](https://github.com/Mrs4s/go-cqhttp). 7 | 8 | v0.2.0及之后使用事件过滤器来进行授权控制, [cqhttp-mirai](https://github.com/yyuueexxiinngg/cqhttp-mirai)暂时不支持此特性, 如果使用[cqhttp-mirai](https://github.com/yyuueexxiinngg/cqhttp-mirai)请从release下载v0.1.5-fix1版本, 请使用v0.2.2.5以上版本的cqhttp-mirai, 否则Bot进群等部分功能将不可用. 9 | 10 | 本项目使用GPL-3.0协议, 您使用本插件所造成的一切财产损失或其他形式损失, 均与开发者无关, 开发者不承担任何相关责任. 11 | 12 | 13 | 相关链接: 14 | 本项目地址: https://github.com/pcrbot/authMS 15 | 16 | HoshinoBot项目地址: https://github.com/Ice-Cirno/HoshinoBot 17 | 18 | ## 特点 19 | * **无GUI** ,不依赖Native环境, 依托于nonebot架构, 对不同的操作系统友好 20 | * 自定义新群试用 21 | * 自定义到期后是否自动退群 22 | * 支持自定义入群发言, 退群发言 23 | * 自定义每日检查频率(最低为每日一次), 可手动快速检查 24 | * 支持无卡密操作, 直接对一个群的授权进行修改/清零 25 | * 支持批量操作(网页/私聊) 26 | * 支持手动提醒群续费(网页) 27 | * 可自动接受好友请求 28 | * 自动检查人数 29 | * 卡密部分自定义 30 | * 授权检查范围广, 支持以下情形: 31 | * 被拉近50人以下的群(因为默认不需要被邀请者同意) 32 | * 超过50人群, 需要机器人同意的群 33 | * 机器人掉线期间被邀请加入的群 34 | * 维护者手动登录机器人帐号加入的群 35 | 36 | 以上情况加入的群聊, 均会收到授权系统的自动检测, 并可自定义退群或开启试用等操作。 37 | 38 | ## 指令示例 39 | **注意, 以下指令中的空格均不可省略** 40 | ### 仅限超级管理员私聊的指令 41 | * 【生成卡密 31*5】生成5张31天的卡密 42 | * 【卡密列表】查看已有卡密的信息,后跟数字来查看对应页数 43 | * 【授权列表】查看所有授权群的信息,后跟数字来查看对应页数 44 | * 【管理员帮助】查看管理员指令 45 | * 【快速检查】立刻检查群的授权, 检查方式与定时任务一样 46 | * 【刷新事件过滤器】手动刷新事件过滤器 47 | 48 | ### 仅限超级管理员的指令 49 | * 【变更授权 123456789+5】为群123456789增加5天授权, 也可以是减 50 | * 【转移授权 123456*987654】将群123456的剩余时间转移至群987654 51 | * 【授权状态】查询此机器人授权信息的统计, 仅限超级管理员 52 | * 【清除授权 987654】清除群987654的全部授权, 并自动退群(如果配置了的话) 53 | * 【退群 987654】命令退出群聊987654, 但并不清除剩余授权时间 54 | * 【变更所有授权 3】为所有已有授权的群增加3天授权时间 55 | * 【不检查人数 987654】不检查群987654的人数是否超标, 直接在群聊中发送则不必附加群号 56 | * 【不检查授权 987654】不检查群987654的授权是否过期, 直接在群聊中发送则不必附加群号 57 | * 【添加白名单 987654】不检查群987654的授权是否过期以及人数是否超标, 直接在群聊中发送则不必附加群号 58 | * 【移除白名单 987654】将群987654从白名单中移出 59 | * 【全部白名单】查询全部白名单信息 60 | 61 | 62 | ### 通用指令 63 | * 【检验卡密 abcdefghijklemop】检查卡密的有效性 64 | * 【充值 abcdefghijklemop】群聊使用,为本群充值 65 | * 【充值 abcdefghijklemop*123456789】为群123456789充值 66 | * 【充值帮助】查看充值帮助内容 67 | 68 | ## 开始使用 69 | 70 | 1. 在HoshinoBot的modules目录下克隆本项目: 71 | ``` 72 | git clone https://github.com/pcrbot/authMS.git 73 | ``` 74 | 2. 安装依赖, 如下载过慢建议清华镜像: 75 | ``` 76 | pip install -r requirements.txt 77 | ``` 78 | 3. 以`filter.json`放入go-cqhttp目录下. 79 | 4. 在HoshinoBot统一配置目录下保存配置信息,命名为`authMS.py`, 已提供配置样板`authMS.py.example`, 按照注释修改为您需要的配置. 请注意, 必须配置事件过滤器路径. 80 | 81 | 82 | ## 其他 83 | * 日志功能并非Hoshino内置的日志记录, 而是作为单独文件记录. 默认保存位置`log/authMS.log`, 默认只记录卡密使用, 加群退群被踢. 84 | * 支持本机多个机器人数据互通, 详情参考`authMS.py.example`中的注释, SQLite是一个本地化的数据库, 因此不支持网络, 配置目录请注意使用斜杠`/`. 85 | 86 | * 如果您是初次使用authMS, 且希望配置为到期自动退群, 建议保持默认`ENABLE_AUTH`为0, 待完成全部现有群授权后, 再修改. 87 | ## 贡献 88 | [GitHub@wdvxdr1123](https://github.com/wdvxdr1123) 89 | 90 | [GitHub@xhl6699](https://github.com/xhl6666) 91 | 92 | [GitHub@var](https://github.com//var-mixer) 93 | 94 | ## 更新日志 95 | 96 | 97 | ### v0.2.1 98 | 更新时间:2020/9/27 99 | 100 | 此版本主要进行风控规避[#16](https://github.com/pcrbot/authMS/issues/16) 101 | * 配置文件新增以下参数: 102 | * `BEGIN`, 用以自定义卡密开头 103 | * 功能变更与调整: 104 | * 发言与退群之间间隔增大, 并限速调用 105 | * 人数检查纳入日志的记录范围 106 | * 卡密开始部分支持自定义, 总长度不变, 例如PCRkPTNeGFilybVn 107 | * 进群之后不会立刻退群, 而是等待下一次自动检查任务时退群 108 | 109 | 110 | ### v0.2.0 111 | 更新时间:2020/9/15 112 | 113 | 此版本主要针对事件过滤器重构 114 | * 配置文件新增以下参数: 115 | * `EVENT_FILTER`, 事件过滤器配置文件目录. 116 | * 功能变更与调整: 117 | * 直接内置了编译好的vue文件 118 | * 指令变更: 119 | * 新增管理员指令【刷新事件过滤器】 120 | 121 | ### v0.1.5 122 | 更新时间:2020/9/9 123 | 124 | 此版本主要新增日志、人数限制、白名单系统、以及网页授权管理 125 | * 配置文件新增以下参数: 126 | * `DEBUG`, 默认关闭, 开启后会详细记录所有的日志. 127 | * `LOG`, 默认开启, 记录重要日志信息, 如进群退群卡密使用 128 | * `LEAVE_AFTER_DAYS`, 在授权到期X天后再退群(仅当配置`AUTO_LEAVE`为True时有效) 129 | * `MAX_GROUP_NUM`, 最大允许的群人数 130 | * 功能变更与调整: 131 | * 新增人数检查功能, 在群人数超标时可以根据配置自动退群或者发送提醒 132 | * 新增白名单功能, 有三种类型, 不检查授权、不检查人数和全部不检查(注意查询授权出来的的结果不会变, 只是不会自动退群或停止响应消息) 133 | * 更新网页授权管理(by [wdvxdr1123](https://github.com/wdvxdr1123)) 134 | * 退群统一由`util.gun_group()`处理, 退群时将会发送退群原因 135 | * 修改配置文件中一些参数的默认值 136 | * 指令变更: 137 | * 移除无效指令【帮助充值】 138 | * 新增管理员指令【不检查人数】【不检查授权】【添加白名单】【全部白名单】【移除白名单】 139 | * 错误修复: 140 | * 修复管理员帮助无法查看的问题 141 | * 修复web授权管理的漏洞, POST请求没有鉴权(by [wdvxdr1123](https://github.com/wdvxdr1123)) 142 | 143 | 144 | 145 | 146 | 147 | ### v0.1.4 148 | 更新时间:2020/8/26 149 | * 配置文件新增以下参数: 150 | * `ENABLE_WEB`, 是否同时启用web管理(之前需在`__init__.py`中配置) 151 | * `PASSWORD`, web管理密码(之前需在`web_server.py`配置) 152 | * `REG_HELP_GROUP`, 群聊充值帮助文本 153 | * `REG_HELP_PRIVATE`, 私聊充值帮助文本 154 | * `ADMIN_HELP`, 给管理员的帮助文本 155 | * `FRIEND_APPROVE`, 是否自动接受加好友的请求 156 | * 文件结构调整 157 | * 新增指令【快速检查】【充值帮助】【管理员帮助】 158 | * 新增特性: 处理加好友事件 159 | * 新增特性: 与管理后台分离的激活界面(感谢[A-kirami](https://github.com/A-kirami) 160 | 161 | ### v0.1.3 162 | 更新时间:2020/8/22 163 | * 添加`nonebot_plugin.py.example`, 可以实现对yobot的授权控制(虽然早就有了) 164 | * 配置文件新增以下参数: 165 | * `ENABLE_AUTH`, 授权系统自动检查/退群总开关 166 | * `FREQUENCY`, 控制授权系统自动检查的频率 167 | * `GROUPS_IN_PAGE`, 控制每页显示的群的条数 168 | * `CARDS_IN_PAGE`, 控制每页显示的卡密条数 169 | * 移除配置项目:`ALLOW_PRIVETE_CHECK` 170 | * 优化获取全部授权列表时的参数, 大幅降低API调用次数(尤其是在群较多时) 171 | 172 | ### v0.1.2 173 | 更新时间:2020/8/18 174 | 175 | > 本次更新合并了[HoshinoAuthorizeSystem](https://github.com/wdvxdr1123/HoshinoAuthorizeSystem)的功能 176 | 177 | * 新增退群,广播功能 178 | * 现在获取已授权群的时时候,会返回群名了 179 | * 使用指令所需权限不足时均增加了提示 180 | * 精简了转移授权部分的代码 181 | 182 | ### v0.1.1 183 | 更新时间:2020/8/18 184 | * 修正[#1](https://github.com/pcrbot/authMS/issues/1), 在设置新群使用天数为0时, 自动退群设置不起作用而直接退群 185 | * 新增指令, 为所有已有授权的群修改时间 186 | 187 | ### v0.1.0 188 | 更新时间:2020/8/15 189 | * 初版发布 190 | -------------------------------------------------------------------------------- /vue/js/app.64de2849.js: -------------------------------------------------------------------------------- 1 | (function(e){function t(t){for(var r,o,u=t[0],l=t[1],c=t[2],s=0,p=[];sPromise.all([n.e(0),n.e(4)]).then(n.bind(null,"713b")),children:[{path:"",component:()=>Promise.all([n.e(0),n.e(3)]).then(n.bind(null,"8b24"))},{path:"login",component:()=>Promise.all([n.e(0),n.e(8)]).then(n.bind(null,"013f"))},{path:"regkey",component:()=>Promise.all([n.e(0),n.e(2)]).then(n.bind(null,"ad61")),meta:{requireAuth:!0}},{path:"activate",component:()=>Promise.all([n.e(0),n.e(5)]).then(n.bind(null,"7aa6"))},{path:"group",component:()=>Promise.all([n.e(0),n.e(7)]).then(n.bind(null,"d684")),meta:{requireAuth:!0}}]}];w.push({path:"*",component:()=>Promise.all([n.e(0),n.e(6)]).then(n.bind(null,"e51e"))});var P=w;a["a"].use(y["a"]);var j=function(){const e=new y["a"]({scrollBehavior:()=>({x:0,y:0}),routes:P,mode:"hash",base:""});return e.beforeEach((e,t,n)=>{e.meta.requireAuth&&!c["a"].getItem("login")?n({path:"/login",query:{to:e.path}}):n()}),e},O=function(){return S.apply(this,arguments)};function S(){return S=o()((function*(){const e="function"===typeof j?yield j({Vue:a["a"]}):j,t={router:e,render:e=>e(v),el:"#q-app"};return{app:t,router:e}})),S.apply(this,arguments)}var x=n("bc3a"),E=n.n(x),_=n("4bde"),q=n("2a19");E.a.interceptors.request.use(e=>(e.baseURL="api/",e.headers["Content-Type"]||(e.headers["Content-Type"]="application/json"),e)),E.a.interceptors.response.use(e=>Promise.resolve(e),e=>{switch(e.response.status){case 401:window.location.href="/#/logout"}return q["a"].create({message:`[${e.response.statusText}]${e.response.data}`,position:"top",color:"negative"}),Promise.reject(e)});var A=Object(_["boot"])(({Vue:e})=>{e.prototype.$axios=E.a});const C="";function T(){return k.apply(this,arguments)}function k(){return k=o()((function*(){const{app:e,router:t}=yield O();let n=!1;const r=e=>{n=!0;const r=Object(e)===e?t.resolve(e).route.fullPath:e;window.location.href=r},o=window.location.href.replace(window.location.origin,""),i=[A];for(let l=0;!1===n&&lPromise.all([n.e(0),n.e(5)]).then(n.bind(null,"713b")),children:[{path:"",component:()=>Promise.all([n.e(0),n.e(4)]).then(n.bind(null,"8b24"))},{path:"login",component:()=>Promise.all([n.e(0),n.e(8)]).then(n.bind(null,"013f"))},{path:"regkey",component:()=>Promise.all([n.e(0),n.e(3)]).then(n.bind(null,"ad61")),meta:{requireAuth:!0}},{path:"activate",component:()=>Promise.all([n.e(0),n.e(6)]).then(n.bind(null,"7aa6"))},{path:"group",component:()=>Promise.all([n.e(0),n.e(2)]).then(n.bind(null,"d684")),meta:{requireAuth:!0}}]}];P.push({path:"*",component:()=>Promise.all([n.e(0),n.e(7)]).then(n.bind(null,"e51e"))});var j=P;a["a"].use(w["a"]);var O=function(){const e=new w["a"]({scrollBehavior:()=>({x:0,y:0}),routes:j,mode:"hash",base:""});return e.beforeEach((e,t,n)=>{e.meta.requireAuth&&!c["a"].getItem("login")?n({path:"/login",query:{to:e.path}}):n()}),e},S=function(){return x.apply(this,arguments)};function x(){return x=o()((function*(){const e="function"===typeof O?yield O({Vue:a["a"]}):O,t={router:e,render:e=>e(y),el:"#q-app"};return{app:t,router:e}})),x.apply(this,arguments)}var E=n("bc3a"),_=n.n(E),q=n("4bde");_.a.interceptors.request.use(e=>(e.baseURL="api/",e.headers["Content-Type"]||(e.headers["Content-Type"]="application/json"),e)),_.a.interceptors.response.use(e=>Promise.resolve(e),e=>{switch(e.response.status){case 401:window.location.href="/#/logout"}return f["a"].create({message:`[${e.response.statusText}]${e.response.data}`,position:"top",color:"negative"}),Promise.reject(e)});var A=Object(q["boot"])(({Vue:e})=>{e.prototype.$axios=_.a});const C="";function T(){return k.apply(this,arguments)}function k(){return k=o()((function*(){const{app:e,router:t}=yield S();let n=!1;const r=e=>{n=!0;const r=Object(e)===e?t.resolve(e).route.fullPath:e;window.location.href=r},o=window.location.href.replace(window.location.origin,""),i=[A];for(let l=0;!1===n&&lPromise.all([n.e(0),n.e(5)]).then(n.bind(null,"713b")),children:[{path:"",component:()=>Promise.all([n.e(0),n.e(4)]).then(n.bind(null,"8b24"))},{path:"login",component:()=>Promise.all([n.e(0),n.e(8)]).then(n.bind(null,"013f"))},{path:"regkey",component:()=>Promise.all([n.e(0),n.e(3)]).then(n.bind(null,"ad61")),meta:{requireAuth:!0}},{path:"activate",component:()=>Promise.all([n.e(0),n.e(6)]).then(n.bind(null,"7aa6"))},{path:"group",component:()=>Promise.all([n.e(0),n.e(2)]).then(n.bind(null,"d684")),meta:{requireAuth:!0}}]}];P.push({path:"*",component:()=>Promise.all([n.e(0),n.e(7)]).then(n.bind(null,"e51e"))});var j=P;a["a"].use(w["a"]);var O=function(){const e=new w["a"]({scrollBehavior:()=>({x:0,y:0}),routes:j,mode:"hash",base:""});return e.beforeEach((e,t,n)=>{e.meta.requireAuth&&!c["a"].getItem("login")?n({path:"/login",query:{to:e.path}}):n()}),e},S=function(){return x.apply(this,arguments)};function x(){return x=o()((function*(){const e="function"===typeof O?yield O({Vue:a["a"]}):O,t={router:e,render:e=>e(y),el:"#q-app"};return{app:t,router:e}})),x.apply(this,arguments)}var E=n("bc3a"),_=n.n(E),q=n("4bde");_.a.interceptors.request.use(e=>(e.baseURL="api/",e.headers["Content-Type"]||(e.headers["Content-Type"]="application/json"),e)),_.a.interceptors.response.use(e=>Promise.resolve(e),e=>{switch(e.response.status){case 401:window.location.href="/#/logout"}return f["a"].create({message:`[${e.response.statusText}]${e.response.data}`,position:"top",color:"negative"}),Promise.reject(e)});var A=Object(q["boot"])(({Vue:e})=>{e.prototype.$axios=_.a});const C="";function T(){return k.apply(this,arguments)}function k(){return k=o()((function*(){const{app:e,router:t}=yield S();let n=!1;const r=e=>{n=!0;const r=Object(e)===e?t.resolve(e).route.fullPath:e;window.location.href=r},o=window.location.href.replace(window.location.origin,""),i=[A];for(let l=0;!1===n&&l pages_all: 61 | await session.finish(f'没有那么多页, 当前共有卡密共{length}条, 共{pages_all}页') 62 | if page <= 0: 63 | await session.finish('请输入正确的页码') 64 | 65 | if not length: 66 | await session.finish('无可用卡密信息') 67 | 68 | msg = '======卡密列表======\n' 69 | i = 0 70 | for items in key_list: 71 | i = i + 1 72 | if i < (page-1)*cards_in_page+1 or i > page*cards_in_page: 73 | continue 74 | msg += '卡密:' + items['key'] + '\n时长:' + str(items['duration']) + '天\n' 75 | msg += f'第{page}页, 共{pages_all}页\n发送卡密列表+页码以查询其他页' 76 | await session.send(msg) 77 | 78 | 79 | @on_command('充值', only_to_me=False) 80 | async def reg_group_chat(session: CommandSession): 81 | 82 | key = session.get('key', prompt="请输入卡密(直接发送)") 83 | if len(key) != 16: 84 | session.finish('卡密错误,请检查后重新开始。') 85 | days = util.query_key(key) 86 | if days == 0: 87 | session.finish("卡密无效,请检查后重新开始。") 88 | if session.event.detail_type == 'private': 89 | # 私聊充值 90 | end = "\n如果我尚未加入您的群聊,您可以邀请我进群~" 91 | gid = session.get('group_id', prompt=f"卡密有效,时长{days}天。\n请输入群号(直接发送)") 92 | if not gid.isdigit(): 93 | session.finish("群号错误,请重新开始。") 94 | else: 95 | end = "" 96 | gid = session.event.group_id 97 | 98 | if gid is None: 99 | return 100 | if session.event.detail_type == 'private': 101 | sid = session.event.self_id 102 | group_name = await util.get_group_name(sid, gid) 103 | msg = f"""[CQ:image,file={get_group_acatar_url(int(gid))}]\n 104 | 群号:{gid} 105 | 群名:{group_name} 106 | ※请确认以上信息后,回复“确认”来完成本次操作。 107 | ※回复其他内容会终止。 108 | """ 109 | ensure = session.get("ensure", prompt=msg) 110 | if ensure != "确认": 111 | session.finish("已取消本次充值") 112 | days = util.query_key(key) 113 | result = await util.reg_group(gid, key) 114 | print(result) 115 | if not result: 116 | # 充值失败 117 | msg = '卡密无效, 请检查是否有误或已被使用, 如果无此类问题请联系发卡方' 118 | else: 119 | nickname = await util.get_nickname(user_id=session.event.user_id) 120 | log_info = f'{nickname}({session.event.user_id})使用了卡密{key}\n为群{gid}成功充值{days}天' 121 | util.log(log_info, 'card_use') 122 | await util.notify_master(log_info) 123 | msg = await util.process_group_msg(gid, result, '充值成功\n', end) 124 | session.finish(msg) 125 | 126 | 127 | @on_command('检验卡密',aliases=('检查卡密'), only_to_me=False) 128 | async def check_card_chat(session): 129 | if not session.current_arg: 130 | await session.finish('检验卡密请发送“检验卡密 卡密”哦~') 131 | else: 132 | origin = session.current_arg.strip() 133 | pattern = re.compile(r'^(\w{16})$') 134 | m = pattern.match(origin) 135 | if m is None: 136 | await session.finish('格式输错了啦憨批!请按照“检验卡密 卡密”进行输入!') 137 | key = m.group(1) 138 | if duration := util.query_key(key): 139 | util.log(f'{session.event.user_id}检查卡密{key},有效期{duration}天') 140 | await session.finish(f'该卡密有效!\n授权时长:{duration}天') 141 | else: 142 | util.log(f'{session.event.user_id}检查卡密{key},无效') 143 | await session.finish(f'该卡密无效!') 144 | 145 | 146 | @on_command('查询授权', only_to_me=False) 147 | async def auth_query_chat(session): 148 | uid = session.event.user_id 149 | if not session.current_arg: 150 | # 无参,检查群聊与否 151 | if session.event.detail_type == 'private': 152 | # 私聊禁止无参数查询授权 153 | await session.finish('私聊查询授权请发送“查询授权 群号”来进行指定群的授权查询(请注意空格)') 154 | return 155 | else: 156 | # 群聊,获取gid 157 | gid = session.event.group_id 158 | else: 159 | # 有参数,检查权限 160 | if uid not in SUPERUSERS: 161 | await session.finish('抱歉,您的权限不足') 162 | return 163 | else: 164 | # 权限为超级管理员 165 | gid = session.current_arg.strip() 166 | if not gid.isdigit(): 167 | await session.finish('请输入正确的群号') 168 | return 169 | 170 | result = util.check_group(gid) 171 | if not result: 172 | msg = '此群未获得授权' 173 | else: 174 | msg = await util.process_group_msg(gid, result, title='授权查询结果\n') 175 | await session.finish(msg) 176 | -------------------------------------------------------------------------------- /group.py: -------------------------------------------------------------------------------- 1 | from nonebot import on_request, on_notice, on_command, NoticeSession 2 | from datetime import datetime 3 | 4 | import pytz 5 | import asyncio 6 | import hoshino, nonebot 7 | 8 | from .constant import config, key_dict, group_dict, trial_list, config 9 | from . import util 10 | 11 | tz = pytz.timezone('Asia/Shanghai') 12 | 13 | 14 | @on_request('group') 15 | async def approve_group_invite(session): 16 | ''' 17 | 自动处理入群邀请 \n 18 | 由于腾讯憨批, 现在被邀请加入50人以下的群不需要验证, 因此此条目只适用于50人以上的邀请的情况, 50人以下请参见下一条函数\n 19 | 请注意, 应当移除其他@on_request('group.add')或者其他入群管理,以防止冲突, 例如移除botmanager下的join_approve \n 20 | 21 | 拒绝入群条件: \n 22 | 1. 新群且试用期设置为0 \n 23 | 2. 群授权已过期(新群试用也视为有授权, 期间内可自由加入) 24 | 25 | v0.1.1后新增, 配置ENABLE_AUTH为假的时候, 不检查授权 26 | ''' 27 | 28 | if not config.ENABLE_AUTH: 29 | # 配置ENABLE_AUTH为0, 则授权系统不起作用, 不会自动通过加群邀请 30 | return 31 | 32 | if session.event.sub_type != 'invite': 33 | return 34 | gid = session.event.group_id 35 | ev = session.event 36 | 37 | new_group_auth = await util.new_group_check(gid) 38 | if new_group_auth == 'expired' or new_group_auth == 'no trial': 39 | await session.bot.set_group_add_request(flag=ev.flag, 40 | sub_type=ev.sub_type, 41 | approve=False, 42 | reason='此群无授权, 请联系维护组') 43 | util.log(f'接受到加群邀请,群号{gid}授权状态{new_group_auth}, 拒绝加入', 'group_add') 44 | elif new_group_auth == 'authed' or new_group_auth == 'trial': 45 | await session.bot.set_group_add_request(flag=ev.flag, 46 | sub_type=ev.sub_type, 47 | approve=True) 48 | util.log(f'接受到加群邀请,群号{gid}授权状态{new_group_auth}, 同意加入', 'group_add') 49 | else: 50 | pass 51 | 52 | 53 | @on_notice('group_increase') 54 | async def approve_group_invite_auto(session): 55 | ''' 56 | 被邀请加入50人以下群时会自动接受, 此时获得的事件类型为通知而非邀请 \n 57 | 无法处理拒绝入群的邀请, 应当使用退群(如果开启了自动退群的话) 58 | ''' 59 | self_ids = session.bot._wsr_api_clients.keys() 60 | for item in self_ids: 61 | sid = item 62 | gid = session.event.group_id 63 | if int(session.event.user_id) != int(sid): 64 | # 入群的人不是自己 65 | return 66 | rt = await check_number(gid) 67 | 68 | if rt == 'overflow': 69 | # 人数超标不自动试用, 考虑到风控, 也不会立刻退群, 而是在下一次自动检查时退群 70 | new_group_auth = 'no trial' 71 | else: 72 | new_group_auth = await util.new_group_check(gid) 73 | if new_group_auth == 'expired' or new_group_auth == 'no trial': 74 | await util.notify_group(group_id=gid, txt='本群无授权或授权或已过期, 如果您已有卡密可发送[充值帮助]来获取充值') 75 | util.log(f'成功加入群{gid}中,该群授权状态{new_group_auth}, 将在下次计划任务时自动退出', 'group_leave') 76 | hoshino.logger.info(f'成功加入群{gid}中,该群授权状态{new_group_auth}, 将在下次计划任务时自动退出') 77 | elif new_group_auth == 'authed' or new_group_auth == 'trial': 78 | await asyncio.sleep(5) # 别发太快了 79 | # 避免重复try 80 | await util.notify_group(group_id=gid, txt=config.NEW_GROUP_MSG) 81 | util.log(f'成功加入群{gid}中,该群授权状态{new_group_auth}', 'group_add') 82 | hoshino.logger.info(f'成功加入群{gid}中,该群授权状态{new_group_auth}') 83 | 84 | 85 | @on_notice('group_decrease.kick_me') 86 | async def kick_me_alert(session: NoticeSession): 87 | ''' 88 | 被踢出同样记录 89 | ''' 90 | group_id = session.event.group_id 91 | operator_id = session.event.operator_id 92 | util.log(f'被{operator_id}踢出群{group_id}', 'group_kick') 93 | 94 | 95 | @on_command('退群', only_to_me=False) 96 | async def group_leave_chat(session): 97 | ''' 98 | 退群, 并不影响授权, 清除授权请试用清除授权命令 99 | ''' 100 | if session.event.user_id not in hoshino.config.SUPERUSERS: 101 | await session.finish('只有主人才能让我退群哦') 102 | return 103 | gid = int(session.current_arg.strip()) 104 | await session.send('正在褪裙...') 105 | 106 | rt_code = await util.gun_group(group_id=gid, reason='管理员操作') 107 | if rt_code == True: 108 | await session.send(f'已成功退出群{gid}') 109 | util.log(f'已成功退出群{gid}', 'group_leave') 110 | else: 111 | await session.send(f'退出群{gid}时发生错误') 112 | util.log(f'退出群{gid}时发生错误', 'group_leave') 113 | 114 | 115 | @on_command('快速检查', only_to_me=True) 116 | async def quick_check_chat(session): 117 | ''' 118 | 立即执行一次检查, 内容与定时任务一样 119 | ''' 120 | if session.event.user_id not in hoshino.config.SUPERUSERS: 121 | return 122 | await check_auth() 123 | await session.finish('检查完成') 124 | 125 | 126 | async def check_auth(): 127 | ''' 128 | 检查所有已加入群的授权状态, 和人数 129 | ''' 130 | bot = nonebot.get_bot() 131 | 132 | # 该函数会独立地检查一次所有群的人数是否超标 133 | await check_number() 134 | 135 | group_info_all = await util.get_group_list_all() 136 | for group in group_info_all: 137 | gid = group['group_id'] 138 | if gid in group_dict: 139 | util.log(f'正在检查群{gid}的授权......') 140 | # 此群已有授权或曾有授权, 检查是否过期 141 | time_left = group_dict[gid] - datetime.now() 142 | days_left = time_left.days 143 | 144 | rt_code = util.allowlist(gid) 145 | if rt_code == 'no_check' or rt_code == 'no_auth_check': 146 | # 在白名单, 并不会影响过期事件 147 | continue 148 | 149 | if time_left.total_seconds() <= 0: 150 | # 已过期, 检查是否在白名单中 151 | 152 | if config.AUTO_LEAVE and time_left.total_seconds() < -config.LEAVE_AFTER_DAYS * 86400: 153 | # 自动退群且已过期超过LEAVE_AFTER_DAYS天, 如果设置LEAVE_AFTER_DAYS为0则立刻退群 154 | await util.gun_group(group_id=gid, reason='授权过期') 155 | util.log(f'群{gid}授权过期,已自动退群', 'group_leave') 156 | else: 157 | # 不自动退群, 只发消息提醒 158 | t_sp = '【提醒】本群授权已过期\n' 159 | e_sp = '如果需要续费请联系机器人维护' 160 | gname_sp = group['group_name'] 161 | msg_expired = await util.process_group_msg(gid=gid, expiration=group_dict[gid], title=t_sp, 162 | end=e_sp, group_name_sp=gname_sp) 163 | try: 164 | await bot.send_group_msg(group_id=gid, message=msg_expired) 165 | except Exception as e: 166 | util.log(f'向群{gid}发送过期提醒时发生错误{type(e)}') 167 | group_dict.pop(gid) 168 | await util.flush_group() 169 | if days_left < config.REMIND_BRFORE_EXPIRED and days_left >= 0: 170 | # 将要过期 171 | msg_remind = await util.process_group_msg( 172 | gid=gid, 173 | expiration=group_dict[gid], 174 | title=f'【提醒】本群的授权已不足{days_left + 1}天\n', 175 | end='\n如果需要续费请联系机器人维护', 176 | group_name_sp=group['group_name']) 177 | try: 178 | await bot.send_group_msg(group_id=gid, 179 | message=msg_remind) 180 | except Exception as e: 181 | hoshino.logger.error(f'向群{gid}发生到期提醒消息时发生错误{type(e)}') 182 | util.log(f'群{gid}的授权不足{days_left + 1}天') 183 | 184 | elif gid not in trial_list: 185 | # 正常情况下, 无论是50人以上群还是50人以下群, 都需要经过机器人同意或检查 186 | # 但是!如果机器人掉线期间被拉进群试用, 将无法处理试用, 因此有了这部分憨批代码 187 | 188 | if not config.NEW_GROUP_DAYS and config.AUTO_LEAVE: 189 | # 无新群试用机制,直接退群 190 | await util.gun_group(group_id=gid, reason='无授权') 191 | util.log(f'发现无记录而被拉入的新群{gid}, 已退出此群', 'group_leave') 192 | else: 193 | await util.new_group_check(gid) 194 | util.log(f'发现无记录而被拉入的新群{gid}, 已开始试用', 'group_add') 195 | 196 | 197 | async def check_number(group_id=0): 198 | ''' 199 | 检查所有群的成员数量是否符合要求, 当传入group_id时则检查传入的群 200 | ''' 201 | if group_id == 0: 202 | gnums = await util.get_group_info(info_type='member_count') 203 | else: 204 | __gid = group_id 205 | gnums = await util.get_group_info(group_ids=__gid, info_type='member_count') 206 | for gid in gnums: 207 | if gnums[gid] > config.MAX_GROUP_NUM: 208 | # 人数超过, 检测是否在白名单 209 | rt_code = util.allowlist(gid) 210 | if rt_code == 'not in' or rt_code == 'no_check_auth': 211 | util.log(f'群{gid}人数超过设定值, 当前人数{gnums[gid]}, 白名单状态{rt_code}', 'number_check') 212 | if group_id == 0: 213 | # 检查全部群的情况, 可以自动退出 214 | if config.AUTO_LEAVE: 215 | await util.gun_group(group_id=gid, reason='群人数超过管理员设定的最大值') 216 | else: 217 | await util.notify_group(group_id=gid, txt='群人数超过管理员设定的最大值, 请联系管理员') 218 | else: 219 | # 检查单个群的情况, 只通知而不自动退出, 等到下次计划任务时再退出 220 | await util.notify_group(group_id=gid, txt='群人数超过管理员设定的最大值, 请联系管理员') 221 | return 'overflow' 222 | return None 223 | -------------------------------------------------------------------------------- /admin.py: -------------------------------------------------------------------------------- 1 | from .util import notify_group 2 | from nonebot import on_command 3 | from math import ceil 4 | 5 | import hoshino 6 | import re 7 | 8 | from . import util 9 | from .constant import config 10 | 11 | 12 | 13 | @on_command('变更所有授权', aliases=('批量变更', '批量授权'), only_to_me=False) 14 | async def add_time_all_chat(session): 15 | ''' 16 | 为所有已有授权的群增加授权x天, 可用于维护补偿时间等场景 17 | ''' 18 | if session.event.user_id not in hoshino.config.SUPERUSERS: 19 | util.log(f'{session.event.user_id}尝试批量授权, 已拒绝') 20 | await session.finish('只有主人才能批量授权哦') 21 | return 22 | if not session.current_arg: 23 | await session.finish('请发送需要为所有群增加或减少的长, 例如“变更所有授权 7”') 24 | 25 | days = int(session.current_arg.strip()) 26 | 27 | authed_group_list = await util.get_authed_group_list() 28 | for ginfo in authed_group_list: 29 | await util.change_authed_time(ginfo['gid'], days) 30 | util.log(f'已为所有群授权增加{days}天') 31 | await session.finish(f'已为所有群授权增加{days}天') 32 | 33 | 34 | @on_command('授权列表', aliases=('查看授权列表', '查看全部授权', '查询全部授权'), only_to_me=True) 35 | async def group_list_chat(session): 36 | ''' 37 | 此指令获得的是, 所有已经获得授权的群, 其中一些群可能Bot并没有加入 \n 38 | 分页显示, 请在authMS.py中配置 39 | ''' 40 | if session.event.user_id not in hoshino.config.SUPERUSERS: 41 | util.log(f'{session.event.user_id}尝试查看授权列表, 已拒绝') 42 | await session.finish('只有主人才能查看授权列表哦') 43 | return 44 | if session.event.detail_type == 'group': 45 | # 群聊查看授权列表你也是个小天才 46 | await session.finish('请超级管理员私聊机器人查看授权列表') 47 | 48 | if not session.current_arg.strip(): 49 | # 无其他参数默认第一页 50 | page = 1 51 | else: 52 | page = int(session.current_arg.strip()) 53 | 54 | msg = '======授权列表======\n' 55 | 56 | authed_group_list = await util.get_authed_group_list() 57 | length = len(authed_group_list) 58 | 59 | groups_in_page = config.GROUPS_IN_PAGE 60 | pages_all = ceil(length / groups_in_page) # 向上取整 61 | if page > pages_all: 62 | await session.finish(f'没有那么多页, 当前共有授权信息{length}条, 共{pages_all}页') 63 | if page <= 0: 64 | await session.finish('请输入正确的页码') 65 | i = 0 66 | for item in authed_group_list: 67 | i = i + 1 68 | if i < (page - 1) * groups_in_page + 1 or i > page * groups_in_page: 69 | continue 70 | gid = int(item['gid']) 71 | g_time = util.check_group(gid) 72 | msg_new = await util.process_group_msg(gid, 73 | g_time, 74 | title=f'第{i}条信息\n', 75 | end='\n\n', 76 | group_name_sp=item['groupName']) 77 | msg += msg_new 78 | 79 | msg += f'第{page}页, 共{pages_all}页\n发送查询授权+页码以查询其他页' 80 | await session.send(msg) 81 | 82 | 83 | @on_command('变更授权', aliases=('更改时间', '授权', '更改授权时间', '更新授权'), only_to_me=False) 84 | async def add_time_chat(session): 85 | origin = session.current_arg.strip() 86 | pattern = re.compile(r'^(\d{5,15})([+-]\d{1,5})$') 87 | m = pattern.match(origin) 88 | if m is None: 89 | await session.finish('请发送“授权 群号±时长”来进行指定群的授权, 时长最长为99999') 90 | gid = int(m.group(1)) 91 | days = int(m.group(2)) 92 | 93 | if session.event.user_id not in hoshino.config.SUPERUSERS: 94 | util.log(f'{session.event.user_id}尝试为群{gid}增加{days}天授权, 已拒绝') 95 | await session.finish('只有主人才能变更授权哦') 96 | return 97 | 98 | result = await util.change_authed_time(gid, days) 99 | msg = await util.process_group_msg(gid, result, title='变更成功, 变更后的群授权信息:\n') 100 | await notify_group(group_id=gid, txt=f'机器人管理员已为本群增加{days}天授权时长,可在群内发送【查询授权】来查看到期时间。') 101 | await session.finish(msg) 102 | 103 | 104 | 105 | @on_command('转移授权', only_to_me=False) 106 | async def group_change_chat(session): 107 | if not session.current_arg: 108 | await session.finish('请发送“转移授权 旧群群号*新群群号”来进行群授权转移') 109 | origin = session.current_arg.strip() 110 | pattern = re.compile(r'^(\d{5,15})\*(\d{5,15})$') 111 | m = pattern.match(origin) 112 | if m is None: 113 | await session.finish('格式错误或者群号错误XD\n请发送“转移授权 旧群群号*新群群号”来转移群授权时长\n如果新群已经授权,则会增加对应时长。') 114 | old_gid = int(m.group(1)) 115 | new_gid = int(m.group(2)) 116 | 117 | if session.event.user_id not in hoshino.config.SUPERUSERS: 118 | util.log(f'{session.event.user_id}尝试转移授权{old_gid}到{new_gid}, 已拒绝') 119 | session.finish('只有主人才能转移授权哦') 120 | return 121 | 122 | gtime_old = util.check_group(old_gid) 123 | if gtime_old == 0: 124 | await session.finish('旧群无授权, 不可进行转移') 125 | if old_gid == new_gid: 126 | await session.finish('宁搁这儿原地TP呢?') 127 | 128 | await util.transfer_group(old_gid, new_gid) 129 | gtime_new = util.check_group(new_gid) 130 | msg = await util.process_group_msg(new_gid,expiration=gtime_new, title=f'旧群{old_gid}授权已清空, 新群授权状态:\n') 131 | await notify_group(group_id=old_gid, txt=f'机器人管理员已转移本群授权时长至其他群。') 132 | 133 | await session.finish(msg) 134 | 135 | @on_command('授权状态', only_to_me=False) 136 | async def auth_status_chat(session): 137 | if session.event.user_id not in hoshino.config.SUPERUSERS: 138 | util.log(f'{session.event.user_id}尝试查看授权状态, 已拒绝') 139 | await session.finish('只有主人才能查看授权状态哦') 140 | return 141 | for sid in hoshino.get_self_ids(): 142 | sgl = set(g['group_id'] 143 | for g in await session.bot.get_group_list(self_id=sid)) 144 | frl = set(f['user_id'] 145 | for f in await session.bot.get_friend_list(self_id=sid)) 146 | # 直接从service里抄了, 面向cv编程才是真 147 | gp_num = len(sgl) 148 | fr_num = len(frl) 149 | key_num = len(util.get_key_list()) 150 | agp_num = len(await util.get_authed_group_list()) 151 | msg = f'Bot账号:{sid}\n所在群数:{gp_num}\n好友数:{fr_num}\n授权群数:{agp_num}\n未使用卡密数:{key_num}' 152 | await session.send(msg) 153 | 154 | 155 | @on_command('清除授权', aliases=('删除授权', '移除授权', '移除群授权', '删除群授权'), only_to_me=True) 156 | async def remove_auth_chat(session): 157 | ''' 158 | 完全移除一个群的授权 \n 159 | 不需要二次确认, 我寻思着你rm /* -rf的时候也没人让你二次确认啊 \n 160 | ''' 161 | if not session.current_arg.strip(): 162 | await session.finish('请输入正确的群号, 例如“清除授权 123456789”') 163 | gid = int(session.current_arg.strip()) 164 | time_left = util.check_group(gid) 165 | 166 | if session.event.user_id not in hoshino.config.SUPERUSERS: 167 | util.log(f'{session.event.user_id}尝试为群{gid}清除授权, 已拒绝') 168 | await session.finish('只有主人才能清除授权哦') 169 | 170 | if not time_left: 171 | await session.finish('此群未获得授权') 172 | msg = await util.process_group_msg(gid=gid, expiration=time_left, title='已移除授权,原授权信息如下\n') 173 | await util.change_authed_time(gid=gid, operate='clear') 174 | 175 | if config.AUTO_LEAVE: 176 | await util.gun_group(group_id=gid, reason='机器人管理员移除授权') 177 | msg += '\n已尝试退出该群聊' 178 | await session.send(msg) 179 | 180 | 181 | @on_command('不检查人数', aliases=('设置人数白名单'), only_to_me=False) 182 | async def no_number_check_chat(session): 183 | ''' 184 | 不检查一个群的人数是否超过人数限制, 在群聊中发送则为不检查本群 185 | ''' 186 | if session.event.detail_type == 'group': 187 | gid = session.event.group_id 188 | elif session.event.detail_type == 'private': 189 | if not session.current_arg.strip(): 190 | await session.finish('请输入正确的群号, 例如“不检查人数 123456789”') 191 | gid = int(session.current_arg.strip()) 192 | 193 | uid = session.event.user_id 194 | if uid not in hoshino.config.SUPERUSERS: 195 | util.log(f'{uid}尝试为群{gid}清除设置不检查人数, 已拒绝') 196 | await session.finish('只有主人才能设置白名单') 197 | return 198 | 199 | util.allowlist(group_id=gid, operator='add', nocheck='no_number_check') 200 | util.log(f'管理员{uid}已将群{gid}添加至白名单, 类型为不检查人数') 201 | await notify_group(group_id=gid, txt='机器人管理员已添加本群为白名单,将不会检查本群人数是否超标。') 202 | await session.finish(f'已将群{gid}添加至白名单, 类型为不检查人数') 203 | 204 | 205 | @on_command('不检查授权', aliases=('设置授权白名单'), only_to_me=False) 206 | async def no_auth_check_chat(session): 207 | if session.event.detail_type == 'group': 208 | gid = session.event.group_id 209 | elif session.event.detail_type == 'private': 210 | if not session.current_arg.strip(): 211 | await session.finish('请输入正确的群号, 例如“不检查授权 123456789”') 212 | gid = int(session.current_arg.strip()) 213 | 214 | uid = session.event.user_id 215 | if uid not in hoshino.config.SUPERUSERS: 216 | util.log(f'{uid}尝试为群{gid}清除设置不检查授权, 已拒绝') 217 | await session.finish('只有主人才能设置白名单') 218 | return 219 | util.allowlist(group_id=gid, operator='add', nocheck='no_auth_check') 220 | util.log(f'已将群{gid}添加至白名单, 类型为不检查授权') 221 | await notify_group(group_id=gid, txt='机器人管理员已添加本群为白名单,将不会检查本群授权是否过期。') 222 | await session.finish(f'已将群{gid}添加至白名单, 类型为不检查授权') 223 | 224 | 225 | @on_command('添加白名单', only_to_me=False) 226 | async def no_check_chat(session): 227 | ''' 228 | 最高级别白名单, 授权与人数都检查 229 | ''' 230 | if session.event.detail_type == 'group': 231 | gid = session.event.group_id 232 | elif session.event.detail_type == 'private': 233 | if not session.current_arg.strip(): 234 | await session.finish('请输入正确的群号, 例如“添加白名单 123456789”') 235 | gid = int(session.current_arg.strip()) 236 | 237 | uid = session.event.user_id 238 | if uid not in hoshino.config.SUPERUSERS: 239 | util.log(f'{uid}尝试为群{gid}清除设置添加白名单, 已拒绝') 240 | await session.finish('只有主人才能设置白名单') 241 | return 242 | 243 | util.allowlist(group_id=gid, operator='add', nocheck='no_check') 244 | util.log(f'已将群{gid}添加至白名单, 类型为全部不检查') 245 | await notify_group(group_id=gid, txt='机器人管理员已添加本群为白名单,将不会检查本群授权以及人数。') 246 | await session.finish(f'已将群{gid}添加至白名单, 类型为全部不检查') 247 | 248 | 249 | @on_command('移除白名单', aliases=('删除白名单')) 250 | async def remove_allowlist_chat(session): 251 | if not session.current_arg.strip(): 252 | await session.finish('请输入正确的群号, 例如“移除白名单 123456789”') 253 | gid = int(session.current_arg.strip()) 254 | uid = session.event.user_id 255 | 256 | if uid not in hoshino.config.SUPERUSERS: 257 | util.log(f'{uid}尝试移除白名单{gid}, 已拒绝') 258 | await session.finish('只有主人才能移除白名单') 259 | return 260 | 261 | re_code = util.allowlist(group_id=gid, operator='remove') 262 | if re_code == 'not in': 263 | await session.finish(f'群{gid}不在白名单中') 264 | await notify_group(group_id=gid, txt='机器人管理员已移除本群的白名单资格') 265 | util.log(f'已将群{gid}移出白名单') 266 | await session.finish(f'已将群{gid}移出白名单') 267 | 268 | 269 | @on_command('全部白名单', aliases=('白名单列表', '所有白名单')) 270 | async def get_allowlist_chat(session): 271 | if session.event.user_id not in hoshino.config.SUPERUSERS: 272 | util.log(f'{session.event.user_id}尝试查看白名单, 已拒绝') 273 | await session.finish('只有主人才能查看白名单') 274 | return 275 | 276 | allow_list = util.get_list(list_type='allowlist') 277 | 278 | msg = '白名单信息\n' 279 | gids = list(allow_list.keys()) 280 | gname_dir = await util.get_group_info(group_ids=gids, info_type='group_name') 281 | # 考虑到一般没有那么多白名单, 因此此处不做分页 282 | i = 1 283 | for gid in gname_dir: 284 | msg += f'第{i}条: 群号{gid}\n' 285 | gname = gname_dir[gid] 286 | gnocheck = allow_list[gid] 287 | msg += f'群名:{gname}\n类型:{gnocheck}\n\n' 288 | i = i+1 289 | session.finish(msg) 290 | 291 | 292 | @on_command('刷新事件过滤器') 293 | async def reload_ef(session): 294 | if session.event.user_id not in hoshino.config.SUPERUSERS: 295 | util.log(f'{session.event.user_id}刷新事件过滤器, 已拒绝') 296 | await session.finish('只有主人才能刷新事件过滤器') 297 | return 298 | await util.flush_group() 299 | await session.send("刷新成功!") 300 | -------------------------------------------------------------------------------- /vue/img/quasar-logo-full.68ae1645.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 21 | 43 | 45 | 46 | 48 | image/svg+xml 49 | 51 | 52 | 53 | 54 | 55 | 60 | 63 | 66 | 69 | 75 | 79 | 83 | 87 | 91 | 95 | 99 | 103 | 104 | 105 | 106 | 107 | 113 | 118 | 126 | 133 | 142 | 151 | 160 | 169 | 178 | 187 | 188 | 189 | 190 | 191 | 192 | -------------------------------------------------------------------------------- /util.py: -------------------------------------------------------------------------------- 1 | 2 | from hoshino.config.__bot__ import SUPERUSERS 3 | from datetime import timedelta, datetime 4 | from math import ceil 5 | 6 | import random, string 7 | import os 8 | import time, json 9 | import asyncio 10 | import nonebot, hoshino 11 | 12 | from .constant import key_dict, group_dict, trial_list, config 13 | 14 | 15 | 16 | def generate_key(): 17 | ''' 18 | 生成16位卡密 19 | ''' 20 | begin=config.BEGIN 21 | new_key = ''.join(random.sample(string.ascii_letters + string.digits, 16-len(begin))) 22 | while new_key in key_dict: # 防止生成重复的卡密, 不过概率太低了。。。 23 | new_key = ''.join(random.sample(string.ascii_letters + string.digits, 16-len(begin))) 24 | new_key = begin + new_key 25 | return new_key 26 | 27 | 28 | def add_key(duration): 29 | ''' 30 | 卡密添加到数据库中 31 | ''' 32 | new_key = generate_key() 33 | key_dict[new_key] = duration 34 | return new_key 35 | 36 | 37 | def get_key_list(): 38 | ''' 39 | 获取全部的卡密列表 40 | ''' 41 | key_list = [] 42 | for key, value in key_dict.iteritems(): 43 | key_list.append({'key': key, 'duration': value}) 44 | return key_list 45 | 46 | 47 | def del_key(key): 48 | ''' 49 | 删除一张卡密, 成功返回True 50 | ''' 51 | if key in key_dict: 52 | key_dict.pop(key) 53 | return True 54 | return False 55 | 56 | 57 | def update_key(key, duration): 58 | ''' 59 | 更新一张卡密,成功返回True 60 | ''' 61 | if duration <= 0: 62 | # 禁止将有效期更新为0, 因为检验卡密时以0为无效标志 63 | return False 64 | if key in key_dict: 65 | key_dict[key] = duration 66 | return True 67 | return False 68 | 69 | 70 | def query_key(key): 71 | ''' 72 | 检查一张卡密, 有效则返回可以增加的授权时间, 无效则返回0 73 | ''' 74 | if key in key_dict: 75 | return key_dict[key] 76 | else: 77 | return 0 78 | 79 | 80 | def check_group(gid): 81 | ''' 82 | 检查一个群是否有授权, 如果有返回过期时间(datetime格式), 否则返回False. 83 | 注意无论Bot是否加入此群都会返回 84 | ''' 85 | if gid in group_dict: 86 | return group_dict[gid] 87 | else: 88 | return 0 89 | 90 | 91 | async def reg_group(gid, key): 92 | ''' 93 | 为一个群充值, 卡密无效则返回False, 否则返回剩余有效期(datatime格式) 94 | ''' 95 | days = query_key(key) 96 | if days == 0: 97 | return False 98 | past_time = await change_authed_time(gid, days) 99 | del_key(key) 100 | return past_time 101 | 102 | 103 | async def change_authed_time(gid, time_change=0, operate=''): 104 | ''' 105 | 不使用卡密, 而直接对一个群的授权时间进行操作 106 | ''' 107 | if operate == 'clear': 108 | try: 109 | # 用try是因为可能会尝试给本来就无授权的群清空授权, 此种情况并不需要另外通知, 因为最终目的一致 110 | group_dict.pop(gid) 111 | except: 112 | pass 113 | await flush_group() 114 | return 0 115 | 116 | if gid in group_dict: 117 | group_dict[gid] = group_dict[gid] + timedelta(days=time_change) 118 | else: 119 | today = datetime.now() 120 | group_dict[gid] = today + timedelta(days=time_change) 121 | await flush_group() 122 | return group_dict[gid] 123 | 124 | async def get_nickname(user_id): 125 | ''' 126 | 获取用户昵称 127 | ''' 128 | uid = user_id 129 | user_info = await nonebot.get_bot().get_stranger_info(user_id=uid) 130 | return user_info['nickname'] 131 | 132 | 133 | async def get_group_info(group_ids=0, info_type='group_name'): 134 | ''' 135 | 1. 传入一个整型数字, 返回单个群指定信息, 格式为字典 136 | 2. 传入一个list, 内含多个群号(int), 返回一个字典, 键为群号, 值为指定信息 137 | 3. 不填入参数, 返回一个包含所有群号与指定信息的字典 138 | 无论获取多少群信息, 均只有一次API的开销, 传入未加入的群时, 将自动忽略 139 | info_type支持group_id, group_name, max_member_count, member_count 140 | ''' 141 | group_info_all = await get_group_list_all() 142 | _gids = [] 143 | _gnames = [] 144 | # 获得的已加入群为列表形式, 处理为需要的字典形式 145 | for it in group_info_all: 146 | _gids.append(it['group_id']) 147 | _gnames.append(it[info_type]) 148 | group_info_dir = dict(zip(_gids, _gnames)) 149 | 150 | if group_ids == 0: 151 | return group_info_dir 152 | if type(group_ids) == int: 153 | # 转为列表 154 | group_ids = [group_ids] 155 | print(group_ids) 156 | 157 | for key in list(group_info_dir.keys()): 158 | if key not in group_ids: 159 | del group_info_dir[key] 160 | else: 161 | # TODO: group not joined 162 | pass 163 | return group_info_dir 164 | 165 | 166 | async def get_authed_group_list(): 167 | ''' 168 | 获取已授权的群 169 | ''' 170 | authed_group_list = [] 171 | group_name_dir = await get_group_info() 172 | 173 | for key, value in group_dict.iteritems(): 174 | deadline = f'{value.year}年{value.month}月{value.day}日 {value.hour}时{value.minute}分' 175 | group_name = group_name_dir.get(int(key), '未知') 176 | authed_group_list.append({'gid': key, 'deadline': deadline, 'groupName': group_name}) 177 | return authed_group_list 178 | 179 | 180 | async def get_group_list_all(): 181 | ''' 182 | 获取所有群, 无论授权与否, 返回为原始类型(列表) 183 | ''' 184 | bot = nonebot.get_bot() 185 | self_ids = bot._wsr_api_clients.keys() 186 | for sid in self_ids: 187 | group_list = await bot.get_group_list(self_id=sid) 188 | return group_list 189 | 190 | 191 | async def process_group_msg(gid, expiration, title: str = '', end='', group_name_sp=''): 192 | ''' 193 | 把查询消息处理为固定的格式 \n 194 | 第一行为额外提醒信息(例如充值成功)\n 195 | 群号:<群号> \n 196 | 群名:<群名> \n 197 | 授权到期:<到期时间> \n 198 | 部分情况下, 可以通过指定群组名的方式来减少对API的调用次数(我并不知道这个对性能是否有大影响) 199 | ''' 200 | if group_name_sp == '': 201 | bot = nonebot.get_bot() 202 | self_ids = bot._wsr_api_clients.keys() 203 | for sid in self_ids: 204 | try: 205 | group_info = await bot.get_group_info(self_id=sid, group_id=gid) 206 | group_name = group_info['group_name'] 207 | except: 208 | group_name = '未知(Bot未加入此群)' 209 | else: 210 | group_name = group_name_sp 211 | time_format = expiration.strftime("%Y-%m-%d %H:%S:%M") 212 | msg = title 213 | msg += f'群号:{gid}\n群名:{group_name}\n到期时间:{time_format}' 214 | msg += end 215 | return msg 216 | 217 | 218 | async def new_group_check(gid): 219 | ''' 220 | 加入新群时检查此群是否符合条件,如果有试用期则会自动添加试用期授权时间, 同时添加试用标志 221 | ''' 222 | if gid in group_dict: 223 | time_left = group_dict[gid] - datetime.now() 224 | 225 | if time_left.total_seconds() <= 0: 226 | # 群在列表但是授权过期, 从授权列表移除此群 227 | group_dict.pop(gid) 228 | await flush_group() 229 | return 'expired' 230 | await flush_group() 231 | return 'authed' 232 | 233 | if config.NEW_GROUP_DAYS <= 0 or gid in trial_list: 234 | return 'no trial' 235 | # 添加试用标记 236 | trial_list[gid] = 1 237 | # 添加试用天数 238 | await change_authed_time(gid=gid, time_change=config.NEW_GROUP_DAYS) 239 | return 'trial' 240 | 241 | 242 | async def transfer_group(old_gid, new_gid): 243 | ''' 244 | 转移授权,新群如果已经有时长了则在现有时长上增加 245 | ''' 246 | today = datetime.now() 247 | left_time = group_dict[old_gid] - today if old_gid in group_dict else timedelta(days=0) 248 | group_dict[new_gid] = left_time + (group_dict[new_gid] if new_gid in group_dict else today) 249 | group_dict.pop(old_gid) 250 | await flush_group() 251 | 252 | async def gun_group(group_id, reason='管理员操作'): 253 | ''' 254 | 退出群聊, 同时会发送消息, 说明退群原因 255 | ''' 256 | gid = group_id 257 | msg = config.GROUP_LEAVE_MSG 258 | msg += reason 259 | try: 260 | await nonebot.get_bot().send_group_msg(group_id=gid, message=msg) 261 | except Exception as e: 262 | hoshino.logger.error(f'向群{group_id}发送退群消息时发生错误{e}') 263 | await asyncio.sleep(2) 264 | try: 265 | await nonebot.get_bot().set_group_leave(group_id=gid) 266 | except nonebot.CQHttpError: 267 | return False 268 | return True 269 | 270 | 271 | async def notify_group(group_id, txt): 272 | ''' 273 | 发送自定义提醒广播,顺带解决了HoshinoBot和Yobot的广播短板 274 | ''' 275 | gid = group_id 276 | try: 277 | await nonebot.get_bot().send_group_msg(group_id=gid, message=txt) 278 | except nonebot.CQHttpError: 279 | return False 280 | return True 281 | 282 | async def notify_master(txt): 283 | ''' 284 | 通知主人 285 | ''' 286 | try: 287 | await nonebot.get_bot().send_private_msg(user_id=SUPERUSERS[0], message=txt) 288 | except nonebot.CQHttpError: 289 | return False 290 | return True 291 | 292 | 293 | def time_now(): 294 | return time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()) 295 | 296 | 297 | LOG_LIST = [ 298 | 'card_use', 299 | 'group_add', 300 | 'group_kick', 301 | 'group_leave', 302 | 'friend_add', 303 | 'number_check' 304 | ] 305 | 306 | 307 | def log(info, log_type='debug'): 308 | ''' 309 | 记录日志, 保存位置为HoshinoBot/logs/authMS.log 310 | ''' 311 | if not config.LOG: 312 | return 313 | if not config.DEBUG and log_type not in LOG_LIST: 314 | # 非DEBUG模式, 非需要记录的信息 315 | return 316 | 317 | file_name = 'log/authMS.log' 318 | with open(file_name, 'a', encoding='utf-8') as l: 319 | l.writelines(f"[{time_now()}]") 320 | l.writelines(info) 321 | l.writelines('\n') 322 | 323 | 324 | def allowlist(group_id, operator='none', nocheck='no_number_check'): 325 | ''' 326 | operator------ 327 | none: 检查一个群或人是否在白名单中, 不在时返回值为not in 328 | add: 增加白名单 329 | del: 移除白名单 330 | clear: 清除所有白名单 331 | nocheck------ 332 | no_number_check: 不检查人数 333 | no_auth_check: 不检查授权(永久有效) 334 | no_check: 全部不检查 335 | ''' 336 | 337 | ALLOWLIST_PATH = os.path.expanduser('~/.hoshino/authMS/allowlist.json') 338 | if os.path.exists(ALLOWLIST_PATH): 339 | with open(ALLOWLIST_PATH, 'r', encoding='utf-8') as rf: 340 | try: 341 | allowlist = json.load(rf) 342 | except Exception as e: 343 | # 您写的json格式有错误?扬了就好了 344 | hoshino.logger.error(f'读取白名单列表时发生错误{type(e)}') 345 | allowlist = {} 346 | else: 347 | os.makedirs(os.path.expanduser('~/.hoshino/authMS'), exist_ok=True) 348 | allowlist = {} 349 | 350 | if operator == 'none': 351 | return allowlist.get(str(group_id), 'not in') 352 | elif operator == 'add': 353 | allowlist[str(group_id)] = nocheck 354 | hoshino.logger.error(f'已将群{group_id}添加到白名单,类型为{nocheck}') 355 | rt = 'ok' 356 | elif operator == 'remove': 357 | rt = allowlist.pop(str(group_id), 'not in') 358 | if rt != 'not in': 359 | hoshino.logger.error(f'已将群{group_id}移除白名单') 360 | rt = 'ok' 361 | else: 362 | hoshino.logger.error(f'群{group_id}移除不在白名单,移除失败') 363 | elif operator == 'clear': 364 | allowlist.clear() 365 | hoshino.logger.error(f'已清空所有白名单') 366 | rt = 'ok' 367 | else: 368 | return 'nothing to do' 369 | 370 | with open(ALLOWLIST_PATH, "w", encoding='utf-8') as sf: 371 | json.dump(allowlist, sf, indent=4, ensure_ascii=False) 372 | 373 | return rt 374 | 375 | 376 | # 这个没写完别看了------------------------------------------- 377 | async def set_block_list(group_id, operator_id, reason='no reason'): 378 | ''' 379 | 将一个群添加到黑名单, 目前仅本地拉黑, 未来可能支持......算了不画饼了 380 | 可以重复拉黑, 以更新reason 381 | 382 | group_id: 要拉黑的群号 383 | operator_id: 操作人 384 | reason: 拉黑原因 385 | 386 | 以下信息将由api自动获取: 387 | group_info: 直接传入bot.get_group_info()返回的原始信息 388 | group_member_list: 直接传入bot.get_group_member_list()返回的群所有成员信息 389 | operator_name: 操作者昵称 390 | ''' 391 | 392 | BLOCKLIST_PATH = os.path.expanduser('~/.hoshino/authMS/blocklist.json') 393 | 394 | # 读名单 395 | if os.path.exists(BLOCKLIST_PATH): 396 | with open(BLOCKLIST_PATH, 'r+', encoding='utf-8') as f: 397 | blocklist = json.load(f) 398 | else: 399 | os.makedirs(os.path.expanduser('~/.hoshino/authMS'), exist_ok=True) 400 | blocklist = {} 401 | 402 | bot = nonebot.get_bot 403 | 404 | # 在对应黑白名单中加入该群, 键值为群号 405 | temp_dir = { 406 | "time": int(time.time()), # 记录为时间戳形式, 便于处理 407 | "operator_id": int(operator_id), 408 | "operator_name": 'name' 409 | } 410 | pass 411 | 412 | 413 | def get_list(list_type='allowlist'): 414 | ''' 415 | list_type可选blocklist和allowlist 416 | 保存位置: ~./.hoshino/authMS/blocklist.json和allowlist.json 417 | 为保持兼容性, 会将所有键值转化为int 418 | ''' 419 | LIST_PATH = os.path.expanduser(f'~/.hoshino/authMS/{list_type}.json') 420 | if os.path.exists(LIST_PATH): 421 | with open(LIST_PATH, 'r', encoding='utf-8') as rf: 422 | try: 423 | ba_list = json.load(rf) 424 | except Exception as e: 425 | print(e) 426 | ba_list = {} 427 | else: 428 | ba_list = {} 429 | 430 | # 将键的str格式转换为int格式, 保持其他函数传参时的兼容性 431 | ba_new = {} 432 | for key in ba_list: 433 | ba_new[int(key)] = ba_list[key] 434 | return ba_new 435 | 436 | 437 | async def flush_group(): 438 | with open(config.EVENT_FILTER, mode="r", encoding='utf-8') as f: 439 | fil = json.load(f) 440 | fil[".or"][0]["group_id"][".in"] = [] 441 | group_list = [] 442 | for key, val in group_dict.iteritems(): 443 | group_list.append(key) 444 | fil[".or"][0]["group_id"][".in"] = group_list 445 | with open(config.EVENT_FILTER, mode="w", encoding='utf-8') as f: 446 | json.dump(fil, f, indent=4, ensure_ascii=False) 447 | await nonebot.get_bot().call_action("reload_event_filter") 448 | -------------------------------------------------------------------------------- /activate/css/style.css: -------------------------------------------------------------------------------- 1 | * { 2 | box-sizing: border-box; 3 | } 4 | main { 5 | position: fixed; 6 | top: 0; 7 | left: 0; 8 | width: 100%; 9 | height: 100%; 10 | display: -webkit-box; 11 | display: -ms-flexbox; 12 | display: flex; 13 | -webkit-box-pack: center; 14 | -ms-flex-pack: center; 15 | justify-content: center; 16 | -webkit-box-align: center; 17 | -ms-flex-align: center; 18 | align-items: center; 19 | background: #ffffff; 20 | } 21 | .form { 22 | display: -webkit-box; 23 | display: -ms-flexbox; 24 | display: flex; 25 | -webkit-box-align: center; 26 | -ms-flex-align: center; 27 | align-items: center; 28 | -webkit-box-pack: center; 29 | -ms-flex-pack: center; 30 | justify-content: center; 31 | position: relative; 32 | width: 400px; 33 | height: 400px; 34 | -ms-flex-negative: 0; 35 | flex-shrink: 0; 36 | padding: 20px; 37 | border-radius: 5px; 38 | } 39 | .form__loader { 40 | display: -webkit-box; 41 | display: -ms-flexbox; 42 | display: flex; 43 | position: absolute; 44 | left: 0; 45 | top: 0; 46 | height: 100%; 47 | width: 100%; 48 | -webkit-box-pack: center; 49 | -ms-flex-pack: center; 50 | justify-content: center; 51 | -webkit-box-align: center; 52 | -ms-flex-align: center; 53 | align-items: center; 54 | z-index: -4; 55 | -webkit-transition: all 0.5s ease; 56 | transition: all 0.5s ease; 57 | } 58 | .form__content { 59 | text-align: center; 60 | display: -webkit-box; 61 | display: -ms-flexbox; 62 | display: flex; 63 | -webkit-box-pack: center; 64 | -ms-flex-pack: center; 65 | justify-content: center; 66 | -webkit-box-orient: vertical; 67 | -webkit-box-direction: normal; 68 | -ms-flex-direction: column; 69 | flex-direction: column; 70 | position: relative; 71 | opacity: 0; 72 | -webkit-transform: translateY(10px); 73 | transform: translateY(10px); 74 | -webkit-transition: all 0.5s ease 0.7s; 75 | transition: all 0.5s ease 0.7s; 76 | width: 90%; 77 | } 78 | .form__cover { 79 | position: absolute; 80 | left: 0; 81 | top: 0; 82 | height: 100%; 83 | width: 100%; 84 | z-index: -4; 85 | border-radius: 7px; 86 | overflow: hidden; 87 | -webkit-transition: all 0.3s ease 0.8s; 88 | transition: all 0.3s ease 0.8s; 89 | box-shadow: 0 0 0 0 rgba(0, 0, 0, 0); 90 | } 91 | .form__cover:after { 92 | content: ''; 93 | position: absolute; 94 | left: 0; 95 | top: 0; 96 | height: 100%; 97 | width: 100%; 98 | background: #ffffff; 99 | z-index: -4; 100 | border-radius: 50%; 101 | -webkit-transition: all 1.5s ease 0.3s; 102 | transition: all 1.5s ease 0.3s; 103 | -webkit-transform: scale(0); 104 | transform: scale(0); 105 | } 106 | .form__cover:before { 107 | content: ''; 108 | position: absolute; 109 | left: 0; 110 | top: 0; 111 | height: 100%; 112 | width: 100%; 113 | background: white; 114 | z-index: -5; 115 | border-radius: 50%; 116 | -webkit-transition: all 0.5s ease; 117 | transition: all 0.5s ease; 118 | -webkit-transform: scale(0); 119 | transform: scale(0); 120 | } 121 | body.on-start .form__cover:before { 122 | -webkit-transform: scale(0.15); 123 | transform: scale(0.15); 124 | } 125 | body.document-loaded .form__loader { 126 | -webkit-transform: scale(0); 127 | transform: scale(0); 128 | opacity: 0; 129 | visibility: hidden; 130 | } 131 | body.document-loaded .form__content { 132 | opacity: 1; 133 | -webkit-transform: none; 134 | transform: none; 135 | } 136 | body.document-loaded .form__cover { 137 | box-shadow: 0 20px 50px rgba(0, 0, 0, 0.3); 138 | } 139 | body.document-loaded .form__cover:after { 140 | -webkit-transform: scale(2); 141 | transform: scale(2); 142 | } 143 | body.document-loaded .form__cover:before { 144 | -webkit-transition: opacity 0.3s ease 0.8s, -webkit-transform 2s ease; 145 | transition: opacity 0.3s ease 0.8s, -webkit-transform 2s ease; 146 | transition: transform 2s ease, opacity 0.3s ease 0.8s; 147 | transition: transform 2s ease, opacity 0.3s ease 0.8s, -webkit-transform 2s ease; 148 | -webkit-transform: scale(2); 149 | transform: scale(2); 150 | opacity: 0; 151 | } 152 | h1 { 153 | font-size: 40px; 154 | margin: 15px 0 20px 0; 155 | letter-spacing: 0.05em; 156 | color: #000000; 157 | font-weight: 700; 158 | } 159 | .styled-button { 160 | -webkit-appearance: none; 161 | -webkit-user-select: none; 162 | cursor: pointer; 163 | font-size: 14px; 164 | width: 100%; 165 | padding: 20px; 166 | outline: none; 167 | background: none; 168 | position: relative; 169 | color: #ffffff; 170 | border-radius: 3px; 171 | margin-bottom: 25px; 172 | border: none; 173 | text-transform: uppercase; 174 | font-weight: 700; 175 | letter-spacing: 0.1em; 176 | background: #108cee; 177 | -webkit-transition: all 0.3s ease; 178 | transition: all 0.3s ease; 179 | overflow: hidden; 180 | } 181 | .styled-button__real-text-holder { 182 | position: relative; 183 | } 184 | .styled-button__real-text { 185 | color: transparent; 186 | display: inline-block; 187 | } 188 | .styled-button__text-holder { 189 | position: absolute; 190 | left: 0; 191 | top: 0; 192 | height: 100%; 193 | width: 100%; 194 | display: -webkit-box; 195 | display: -ms-flexbox; 196 | display: flex; 197 | -webkit-box-align: center; 198 | -ms-flex-align: center; 199 | align-items: center; 200 | -webkit-box-pack: center; 201 | -ms-flex-pack: center; 202 | justify-content: center; 203 | -webkit-transition: all 0.3s ease; 204 | transition: all 0.3s ease; 205 | } 206 | .styled-button__moving-block { 207 | -webkit-transition: all 0.3s ease; 208 | transition: all 0.3s ease; 209 | position: absolute; 210 | left: 0; 211 | top: 0; 212 | height: 100%; 213 | width: 100%; 214 | overflow: hidden; 215 | } 216 | .styled-button__moving-block.back { 217 | color: white; 218 | -webkit-transform: translateX(-100%); 219 | transform: translateX(-100%); 220 | } 221 | .styled-button__moving-block.back .styled-button__text-holder { 222 | -webkit-transform: translateX(100%); 223 | transform: translateX(100%); 224 | } 225 | .styled-button:hover, 226 | .styled-button:active { 227 | box-shadow: 0 8px 20px rgba(0, 0, 0, 0.3); 228 | background: #1198ff; 229 | } 230 | .styled-button:hover .face, 231 | .styled-button:active .face { 232 | -webkit-transform: translateX(100%); 233 | transform: translateX(100%); 234 | } 235 | .styled-button:hover .face .styled-button__text-holder, 236 | .styled-button:active .face .styled-button__text-holder { 237 | -webkit-transform: translateX(-100%); 238 | transform: translateX(-100%); 239 | } 240 | .styled-button:hover .back, 241 | .styled-button:active .back { 242 | -webkit-transform: translateX(0); 243 | transform: translateX(0); 244 | } 245 | .styled-button:hover .back .styled-button__text-holder, 246 | .styled-button:active .back .styled-button__text-holder { 247 | -webkit-transform: translateX(0); 248 | transform: translateX(0); 249 | } 250 | .styled-button:active { 251 | box-shadow: 0 0 5px rgba(0, 0, 0, 0.3); 252 | } 253 | .styled-input { 254 | width: 100%; 255 | position: relative; 256 | margin-bottom: 25px; 257 | border: 1px solid rgba(0, 0, 0, 0.4); 258 | border-radius: 3px; 259 | -webkit-transition: all 0.3s ease; 260 | transition: all 0.3s ease; 261 | } 262 | .styled-input__circle { 263 | position: absolute; 264 | left: 0; 265 | top: 0; 266 | width: 100%; 267 | height: 100%; 268 | z-index: -2; 269 | overflow: hidden; 270 | border-radius: 3px; 271 | } 272 | .styled-input__circle:after { 273 | content: ''; 274 | position: absolute; 275 | left: 16.5px; 276 | top: 19px; 277 | height: 14px; 278 | width: 14px; 279 | z-index: -2; 280 | border-radius: 50%; 281 | background: #a0e2ff; 282 | box-shadow: 0 0 10px rgba(255, 255, 255, 0); 283 | -webkit-transition: opacity 1s ease, -webkit-transform 0.6s ease; 284 | transition: opacity 1s ease, -webkit-transform 0.6s ease; 285 | transition: transform 0.6s ease, opacity 1s ease; 286 | transition: transform 0.6s ease, opacity 1s ease, -webkit-transform 0.6s ease; 287 | } 288 | .styled-input__input { 289 | width: 100%; 290 | -webkit-appearance: none; 291 | font-size: 14px; 292 | outline: none; 293 | background: none; 294 | padding: 18px 15px; 295 | color: #000000; 296 | border: none; 297 | font-weight: 600; 298 | letter-spacing: 0.035em; 299 | } 300 | .styled-input__placeholder { 301 | position: absolute; 302 | left: 0; 303 | top: 0; 304 | width: 100%; 305 | height: 100%; 306 | display: -webkit-box; 307 | display: -ms-flexbox; 308 | display: flex; 309 | -webkit-box-align: center; 310 | -ms-flex-align: center; 311 | align-items: center; 312 | z-index: -1; 313 | padding-left: 45px; 314 | color: #000000; 315 | } 316 | .styled-input__placeholder-text { 317 | -webkit-perspective: 500px; 318 | perspective: 500px; 319 | display: inline-block; 320 | } 321 | .styled-input__placeholder-text .letter { 322 | display: inline-block; 323 | vertical-align: middle; 324 | position: relative; 325 | -webkit-animation: letterAnimOut 0.25s ease forwards; 326 | animation: letterAnimOut 0.25s ease forwards; 327 | /* text-shadow: 0 0 5px; */ 328 | } 329 | .styled-input__placeholder-text .letter.active { 330 | -webkit-animation: letterAnimIn 0.25s ease forwards; 331 | animation: letterAnimIn 0.25s ease forwards; 332 | } 333 | .styled-input:hover { 334 | border-color: #108cee; 335 | } 336 | .styled-input.filled { 337 | border-color: #108cee; 338 | } 339 | .styled-input.filled .styled-input__circle:after { 340 | -webkit-transform: scale(37); 341 | transform: scale(37); 342 | opacity: 0; 343 | } 344 | @-webkit-keyframes letterAnimIn { 345 | 0% { 346 | -webkit-transform: translate(0, 0); 347 | transform: translate(0, 0); 348 | } 349 | 25% { 350 | -webkit-transform: translate(0, 10px); 351 | transform: translate(0, 10px); 352 | color: #9c9c9c; 353 | } 354 | 45% { 355 | -webkit-transform: translate(0, 10px); 356 | transform: translate(0, 10px); 357 | opacity: 0; 358 | color: #9c9c9c; 359 | } 360 | 55% { 361 | -webkit-transform: translate(0, 10px); 362 | transform: translate(0, 10px); 363 | opacity: 0; 364 | } 365 | 56% { 366 | -webkit-transform: translate(-30px, -27px); 367 | transform: translate(-30px, -27px); 368 | opacity: 0; 369 | color: #c9c9c9; 370 | } 371 | 76% { 372 | color: #c9c9c9; 373 | opacity: 1; 374 | -webkit-transform: translate(-30px, -27px); 375 | transform: translate(-30px, -27px); 376 | } 377 | 100% { 378 | -webkit-transform: translate(-30px, -27px); 379 | transform: translate(-30px, -27px); 380 | opacity: 1; 381 | } 382 | } 383 | @keyframes letterAnimIn { 384 | 0% { 385 | -webkit-transform: translate(0, 0); 386 | transform: translate(0, 0); 387 | } 388 | 25% { 389 | -webkit-transform: translate(0, 10px); 390 | transform: translate(0, 10px); 391 | color: #9c9c9c; 392 | } 393 | 45% { 394 | -webkit-transform: translate(0, 10px); 395 | transform: translate(0, 10px); 396 | opacity: 0; 397 | color: #9c9c9c; 398 | } 399 | 55% { 400 | -webkit-transform: translate(0, 10px); 401 | transform: translate(0, 10px); 402 | opacity: 0; 403 | } 404 | 56% { 405 | -webkit-transform: translate(-30px, -27px); 406 | transform: translate(-30px, -27px); 407 | opacity: 0; 408 | color: #c9c9c9; 409 | } 410 | 76% { 411 | color: #c9c9c9; 412 | opacity: 1; 413 | -webkit-transform: translate(-30px, -27px); 414 | transform: translate(-30px, -27px); 415 | } 416 | 100% { 417 | -webkit-transform: translate(-30px, -27px); 418 | transform: translate(-30px, -27px); 419 | opacity: 1; 420 | } 421 | } 422 | @-webkit-keyframes letterAnimOut { 423 | 0% { 424 | -webkit-transform: translate(-30px, -27px); 425 | transform: translate(-30px, -27px); 426 | opacity: 1; 427 | } 428 | 25% { 429 | -webkit-transform: translate(-30px, -40px); 430 | transform: translate(-30px, -40px); 431 | opacity: 0; 432 | } 433 | 45% { 434 | -webkit-transform: translate(0, 10px); 435 | transform: translate(0, 10px); 436 | opacity: 0; 437 | } 438 | 55% { 439 | -webkit-transform: translate(0, 10px); 440 | transform: translate(0, 10px); 441 | opacity: 0; 442 | color: #9c9c9c; 443 | } 444 | 56% { 445 | -webkit-transform: translate(0, 10px); 446 | transform: translate(0, 10px); 447 | color: #9c9c9c; 448 | } 449 | 100% { 450 | -webkit-transform: translate(0, 0); 451 | transform: translate(0, 0); 452 | } 453 | } 454 | @keyframes letterAnimOut { 455 | 0% { 456 | -webkit-transform: translate(-30px, -27px); 457 | transform: translate(-30px, -27px); 458 | opacity: 1; 459 | } 460 | 25% { 461 | -webkit-transform: translate(-30px, -40px); 462 | transform: translate(-30px, -40px); 463 | opacity: 0; 464 | } 465 | 45% { 466 | -webkit-transform: translate(0, 10px); 467 | transform: translate(0, 10px); 468 | opacity: 0; 469 | } 470 | 55% { 471 | -webkit-transform: translate(0, 10px); 472 | transform: translate(0, 10px); 473 | opacity: 0; 474 | color: #9c9c9c; 475 | } 476 | 56% { 477 | -webkit-transform: translate(0, 10px); 478 | transform: translate(0, 10px); 479 | color: #9c9c9c; 480 | } 481 | 100% { 482 | -webkit-transform: translate(0, 0); 483 | transform: translate(0, 0); 484 | } 485 | } 486 | .spinner { 487 | position: relative; 488 | margin: auto; 489 | width: 50px; 490 | height: 50px; 491 | -webkit-transition: all 0.2s ease 0s; 492 | transition: all 0.2s ease 0s; 493 | } 494 | .spinner__circular { 495 | -webkit-animation: rotate 1.5s linear infinite; 496 | animation: rotate 1.5s linear infinite; 497 | -webkit-animation-play-state: paused; 498 | animation-play-state: paused; 499 | -webkit-transform-origin: center center; 500 | transform-origin: center center; 501 | position: absolute; 502 | width: 100%; 503 | height: 100%; 504 | top: 0; 505 | left: 0; 506 | margin: auto; 507 | } 508 | .spinner__path { 509 | stroke-dasharray: 1, 200; 510 | stroke-dashoffset: 0; 511 | -webkit-animation: dash 1.3s ease forwards 0.5s; 512 | animation: dash 1.3s ease forwards 0.5s; 513 | opacity: 0; 514 | stroke-linecap: round; 515 | stroke: #108cee; 516 | -webkit-animation-play-state: running; 517 | animation-play-state: running; 518 | } 519 | @-webkit-keyframes dash { 520 | 0% { 521 | stroke-dasharray: 1, 200; 522 | stroke-dashoffset: 0; 523 | opacity: 0; 524 | } 525 | 50% { 526 | stroke-dasharray: 40, 200; 527 | opacity: 1; 528 | } 529 | 100% { 530 | stroke-dasharray: 125, 200; 531 | opacity: 1; 532 | } 533 | } 534 | @keyframes dash { 535 | 0% { 536 | stroke-dasharray: 1, 200; 537 | stroke-dashoffset: 0; 538 | opacity: 0; 539 | } 540 | 50% { 541 | stroke-dasharray: 40, 200; 542 | opacity: 1; 543 | } 544 | 100% { 545 | stroke-dasharray: 125, 200; 546 | opacity: 1; 547 | } 548 | } 549 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | The GNU General Public License is a free, copyleft license for 11 | software and other kinds of works. 12 | 13 | The licenses for most software and other practical works are designed 14 | to take away your freedom to share and change the works. By contrast, 15 | the GNU General Public License is intended to guarantee your freedom to 16 | share and change all versions of a program--to make sure it remains free 17 | software for all its users. We, the Free Software Foundation, use the 18 | GNU General Public License for most of our software; it applies also to 19 | any other work released this way by its authors. You can apply it to 20 | your programs, too. 21 | 22 | When we speak of free software, we are referring to freedom, not 23 | price. Our General Public Licenses are designed to make sure that you 24 | have the freedom to distribute copies of free software (and charge for 25 | them if you wish), that you receive source code or can get it if you 26 | want it, that you can change the software or use pieces of it in new 27 | free programs, and that you know you can do these things. 28 | 29 | To protect your rights, we need to prevent others from denying you 30 | these rights or asking you to surrender the rights. Therefore, you have 31 | certain responsibilities if you distribute copies of the software, or if 32 | you modify it: responsibilities to respect the freedom of others. 33 | 34 | For example, if you distribute copies of such a program, whether 35 | gratis or for a fee, you must pass on to the recipients the same 36 | freedoms that you received. You must make sure that they, too, receive 37 | or can get the source code. And you must show them these terms so they 38 | know their rights. 39 | 40 | Developers that use the GNU GPL protect your rights with two steps: 41 | (1) assert copyright on the software, and (2) offer you this License 42 | giving you legal permission to copy, distribute and/or modify it. 43 | 44 | For the developers' and authors' protection, the GPL clearly explains 45 | that there is no warranty for this free software. For both users' and 46 | authors' sake, the GPL requires that modified versions be marked as 47 | changed, so that their problems will not be attributed erroneously to 48 | authors of previous versions. 49 | 50 | Some devices are designed to deny users access to install or run 51 | modified versions of the software inside them, although the manufacturer 52 | can do so. This is fundamentally incompatible with the aim of 53 | protecting users' freedom to change the software. The systematic 54 | pattern of such abuse occurs in the area of products for individuals to 55 | use, which is precisely where it is most unacceptable. Therefore, we 56 | have designed this version of the GPL to prohibit the practice for those 57 | products. If such problems arise substantially in other domains, we 58 | stand ready to extend this provision to those domains in future versions 59 | of the GPL, as needed to protect the freedom of users. 60 | 61 | Finally, every program is threatened constantly by software patents. 62 | States should not allow patents to restrict development and use of 63 | software on general-purpose computers, but in those that do, we wish to 64 | avoid the special danger that patents applied to a free program could 65 | make it effectively proprietary. To prevent this, the GPL assures that 66 | patents cannot be used to render the program non-free. 67 | 68 | The precise terms and conditions for copying, distribution and 69 | modification follow. 70 | 71 | TERMS AND CONDITIONS 72 | 73 | 0. Definitions. 74 | 75 | "This License" refers to version 3 of the GNU General Public License. 76 | 77 | "Copyright" also means copyright-like laws that apply to other kinds of 78 | works, such as semiconductor masks. 79 | 80 | "The Program" refers to any copyrightable work licensed under this 81 | License. Each licensee is addressed as "you". "Licensees" and 82 | "recipients" may be individuals or organizations. 83 | 84 | To "modify" a work means to copy from or adapt all or part of the work 85 | in a fashion requiring copyright permission, other than the making of an 86 | exact copy. The resulting work is called a "modified version" of the 87 | earlier work or a work "based on" the earlier work. 88 | 89 | A "covered work" means either the unmodified Program or a work based 90 | on the Program. 91 | 92 | To "propagate" a work means to do anything with it that, without 93 | permission, would make you directly or secondarily liable for 94 | infringement under applicable copyright law, except executing it on a 95 | computer or modifying a private copy. Propagation includes copying, 96 | distribution (with or without modification), making available to the 97 | public, and in some countries other activities as well. 98 | 99 | To "convey" a work means any kind of propagation that enables other 100 | parties to make or receive copies. Mere interaction with a user through 101 | a computer network, with no transfer of a copy, is not conveying. 102 | 103 | An interactive user interface displays "Appropriate Legal Notices" 104 | to the extent that it includes a convenient and prominently visible 105 | feature that (1) displays an appropriate copyright notice, and (2) 106 | tells the user that there is no warranty for the work (except to the 107 | extent that warranties are provided), that licensees may convey the 108 | work under this License, and how to view a copy of this License. If 109 | the interface presents a list of user commands or options, such as a 110 | menu, a prominent item in the list meets this criterion. 111 | 112 | 1. Source Code. 113 | 114 | The "source code" for a work means the preferred form of the work 115 | for making modifications to it. "Object code" means any non-source 116 | form of a work. 117 | 118 | A "Standard Interface" means an interface that either is an official 119 | standard defined by a recognized standards body, or, in the case of 120 | interfaces specified for a particular programming language, one that 121 | is widely used among developers working in that language. 122 | 123 | The "System Libraries" of an executable work include anything, other 124 | than the work as a whole, that (a) is included in the normal form of 125 | packaging a Major Component, but which is not part of that Major 126 | Component, and (b) serves only to enable use of the work with that 127 | Major Component, or to implement a Standard Interface for which an 128 | implementation is available to the public in source code form. A 129 | "Major Component", in this context, means a major essential component 130 | (kernel, window system, and so on) of the specific operating system 131 | (if any) on which the executable work runs, or a compiler used to 132 | produce the work, or an object code interpreter used to run it. 133 | 134 | The "Corresponding Source" for a work in object code form means all 135 | the source code needed to generate, install, and (for an executable 136 | work) run the object code and to modify the work, including scripts to 137 | control those activities. However, it does not include the work's 138 | System Libraries, or general-purpose tools or generally available free 139 | programs which are used unmodified in performing those activities but 140 | which are not part of the work. For example, Corresponding Source 141 | includes interface definition files associated with source files for 142 | the work, and the source code for shared libraries and dynamically 143 | linked subprograms that the work is specifically designed to require, 144 | such as by intimate data communication or control flow between those 145 | subprograms and other parts of the work. 146 | 147 | The Corresponding Source need not include anything that users 148 | can regenerate automatically from other parts of the Corresponding 149 | Source. 150 | 151 | The Corresponding Source for a work in source code form is that 152 | same work. 153 | 154 | 2. Basic Permissions. 155 | 156 | All rights granted under this License are granted for the term of 157 | copyright on the Program, and are irrevocable provided the stated 158 | conditions are met. This License explicitly affirms your unlimited 159 | permission to run the unmodified Program. The output from running a 160 | covered work is covered by this License only if the output, given its 161 | content, constitutes a covered work. This License acknowledges your 162 | rights of fair use or other equivalent, as provided by copyright law. 163 | 164 | You may make, run and propagate covered works that you do not 165 | convey, without conditions so long as your license otherwise remains 166 | in force. You may convey covered works to others for the sole purpose 167 | of having them make modifications exclusively for you, or provide you 168 | with facilities for running those works, provided that you comply with 169 | the terms of this License in conveying all material for which you do 170 | not control copyright. Those thus making or running the covered works 171 | for you must do so exclusively on your behalf, under your direction 172 | and control, on terms that prohibit them from making any copies of 173 | your copyrighted material outside their relationship with you. 174 | 175 | Conveying under any other circumstances is permitted solely under 176 | the conditions stated below. Sublicensing is not allowed; section 10 177 | makes it unnecessary. 178 | 179 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 180 | 181 | No covered work shall be deemed part of an effective technological 182 | measure under any applicable law fulfilling obligations under article 183 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 184 | similar laws prohibiting or restricting circumvention of such 185 | measures. 186 | 187 | When you convey a covered work, you waive any legal power to forbid 188 | circumvention of technological measures to the extent such circumvention 189 | is effected by exercising rights under this License with respect to 190 | the covered work, and you disclaim any intention to limit operation or 191 | modification of the work as a means of enforcing, against the work's 192 | users, your or third parties' legal rights to forbid circumvention of 193 | technological measures. 194 | 195 | 4. Conveying Verbatim Copies. 196 | 197 | You may convey verbatim copies of the Program's source code as you 198 | receive it, in any medium, provided that you conspicuously and 199 | appropriately publish on each copy an appropriate copyright notice; 200 | keep intact all notices stating that this License and any 201 | non-permissive terms added in accord with section 7 apply to the code; 202 | keep intact all notices of the absence of any warranty; and give all 203 | recipients a copy of this License along with the Program. 204 | 205 | You may charge any price or no price for each copy that you convey, 206 | and you may offer support or warranty protection for a fee. 207 | 208 | 5. Conveying Modified Source Versions. 209 | 210 | You may convey a work based on the Program, or the modifications to 211 | produce it from the Program, in the form of source code under the 212 | terms of section 4, provided that you also meet all of these conditions: 213 | 214 | a) The work must carry prominent notices stating that you modified 215 | it, and giving a relevant date. 216 | 217 | b) The work must carry prominent notices stating that it is 218 | released under this License and any conditions added under section 219 | 7. This requirement modifies the requirement in section 4 to 220 | "keep intact all notices". 221 | 222 | c) You must license the entire work, as a whole, under this 223 | License to anyone who comes into possession of a copy. This 224 | License will therefore apply, along with any applicable section 7 225 | additional terms, to the whole of the work, and all its parts, 226 | regardless of how they are packaged. This License gives no 227 | permission to license the work in any other way, but it does not 228 | invalidate such permission if you have separately received it. 229 | 230 | d) If the work has interactive user interfaces, each must display 231 | Appropriate Legal Notices; however, if the Program has interactive 232 | interfaces that do not display Appropriate Legal Notices, your 233 | work need not make them do so. 234 | 235 | A compilation of a covered work with other separate and independent 236 | works, which are not by their nature extensions of the covered work, 237 | and which are not combined with it such as to form a larger program, 238 | in or on a volume of a storage or distribution medium, is called an 239 | "aggregate" if the compilation and its resulting copyright are not 240 | used to limit the access or legal rights of the compilation's users 241 | beyond what the individual works permit. Inclusion of a covered work 242 | in an aggregate does not cause this License to apply to the other 243 | parts of the aggregate. 244 | 245 | 6. Conveying Non-Source Forms. 246 | 247 | You may convey a covered work in object code form under the terms 248 | of sections 4 and 5, provided that you also convey the 249 | machine-readable Corresponding Source under the terms of this License, 250 | in one of these ways: 251 | 252 | a) Convey the object code in, or embodied in, a physical product 253 | (including a physical distribution medium), accompanied by the 254 | Corresponding Source fixed on a durable physical medium 255 | customarily used for software interchange. 256 | 257 | b) Convey the object code in, or embodied in, a physical product 258 | (including a physical distribution medium), accompanied by a 259 | written offer, valid for at least three years and valid for as 260 | long as you offer spare parts or customer support for that product 261 | model, to give anyone who possesses the object code either (1) a 262 | copy of the Corresponding Source for all the software in the 263 | product that is covered by this License, on a durable physical 264 | medium customarily used for software interchange, for a price no 265 | more than your reasonable cost of physically performing this 266 | conveying of source, or (2) access to copy the 267 | Corresponding Source from a network server at no charge. 268 | 269 | c) Convey individual copies of the object code with a copy of the 270 | written offer to provide the Corresponding Source. This 271 | alternative is allowed only occasionally and noncommercially, and 272 | only if you received the object code with such an offer, in accord 273 | with subsection 6b. 274 | 275 | d) Convey the object code by offering access from a designated 276 | place (gratis or for a charge), and offer equivalent access to the 277 | Corresponding Source in the same way through the same place at no 278 | further charge. You need not require recipients to copy the 279 | Corresponding Source along with the object code. If the place to 280 | copy the object code is a network server, the Corresponding Source 281 | may be on a different server (operated by you or a third party) 282 | that supports equivalent copying facilities, provided you maintain 283 | clear directions next to the object code saying where to find the 284 | Corresponding Source. Regardless of what server hosts the 285 | Corresponding Source, you remain obligated to ensure that it is 286 | available for as long as needed to satisfy these requirements. 287 | 288 | e) Convey the object code using peer-to-peer transmission, provided 289 | you inform other peers where the object code and Corresponding 290 | Source of the work are being offered to the general public at no 291 | charge under subsection 6d. 292 | 293 | A separable portion of the object code, whose source code is excluded 294 | from the Corresponding Source as a System Library, need not be 295 | included in conveying the object code work. 296 | 297 | A "User Product" is either (1) a "consumer product", which means any 298 | tangible personal property which is normally used for personal, family, 299 | or household purposes, or (2) anything designed or sold for incorporation 300 | into a dwelling. In determining whether a product is a consumer product, 301 | doubtful cases shall be resolved in favor of coverage. For a particular 302 | product received by a particular user, "normally used" refers to a 303 | typical or common use of that class of product, regardless of the status 304 | of the particular user or of the way in which the particular user 305 | actually uses, or expects or is expected to use, the product. A product 306 | is a consumer product regardless of whether the product has substantial 307 | commercial, industrial or non-consumer uses, unless such uses represent 308 | the only significant mode of use of the product. 309 | 310 | "Installation Information" for a User Product means any methods, 311 | procedures, authorization keys, or other information required to install 312 | and execute modified versions of a covered work in that User Product from 313 | a modified version of its Corresponding Source. The information must 314 | suffice to ensure that the continued functioning of the modified object 315 | code is in no case prevented or interfered with solely because 316 | modification has been made. 317 | 318 | If you convey an object code work under this section in, or with, or 319 | specifically for use in, a User Product, and the conveying occurs as 320 | part of a transaction in which the right of possession and use of the 321 | User Product is transferred to the recipient in perpetuity or for a 322 | fixed term (regardless of how the transaction is characterized), the 323 | Corresponding Source conveyed under this section must be accompanied 324 | by the Installation Information. But this requirement does not apply 325 | if neither you nor any third party retains the ability to install 326 | modified object code on the User Product (for example, the work has 327 | been installed in ROM). 328 | 329 | The requirement to provide Installation Information does not include a 330 | requirement to continue to provide support service, warranty, or updates 331 | for a work that has been modified or installed by the recipient, or for 332 | the User Product in which it has been modified or installed. Access to a 333 | network may be denied when the modification itself materially and 334 | adversely affects the operation of the network or violates the rules and 335 | protocols for communication across the network. 336 | 337 | Corresponding Source conveyed, and Installation Information provided, 338 | in accord with this section must be in a format that is publicly 339 | documented (and with an implementation available to the public in 340 | source code form), and must require no special password or key for 341 | unpacking, reading or copying. 342 | 343 | 7. Additional Terms. 344 | 345 | "Additional permissions" are terms that supplement the terms of this 346 | License by making exceptions from one or more of its conditions. 347 | Additional permissions that are applicable to the entire Program shall 348 | be treated as though they were included in this License, to the extent 349 | that they are valid under applicable law. If additional permissions 350 | apply only to part of the Program, that part may be used separately 351 | under those permissions, but the entire Program remains governed by 352 | this License without regard to the additional permissions. 353 | 354 | When you convey a copy of a covered work, you may at your option 355 | remove any additional permissions from that copy, or from any part of 356 | it. (Additional permissions may be written to require their own 357 | removal in certain cases when you modify the work.) You may place 358 | additional permissions on material, added by you to a covered work, 359 | for which you have or can give appropriate copyright permission. 360 | 361 | Notwithstanding any other provision of this License, for material you 362 | add to a covered work, you may (if authorized by the copyright holders of 363 | that material) supplement the terms of this License with terms: 364 | 365 | a) Disclaiming warranty or limiting liability differently from the 366 | terms of sections 15 and 16 of this License; or 367 | 368 | b) Requiring preservation of specified reasonable legal notices or 369 | author attributions in that material or in the Appropriate Legal 370 | Notices displayed by works containing it; or 371 | 372 | c) Prohibiting misrepresentation of the origin of that material, or 373 | requiring that modified versions of such material be marked in 374 | reasonable ways as different from the original version; or 375 | 376 | d) Limiting the use for publicity purposes of names of licensors or 377 | authors of the material; or 378 | 379 | e) Declining to grant rights under trademark law for use of some 380 | trade names, trademarks, or service marks; or 381 | 382 | f) Requiring indemnification of licensors and authors of that 383 | material by anyone who conveys the material (or modified versions of 384 | it) with contractual assumptions of liability to the recipient, for 385 | any liability that these contractual assumptions directly impose on 386 | those licensors and authors. 387 | 388 | All other non-permissive additional terms are considered "further 389 | restrictions" within the meaning of section 10. If the Program as you 390 | received it, or any part of it, contains a notice stating that it is 391 | governed by this License along with a term that is a further 392 | restriction, you may remove that term. If a license document contains 393 | a further restriction but permits relicensing or conveying under this 394 | License, you may add to a covered work material governed by the terms 395 | of that license document, provided that the further restriction does 396 | not survive such relicensing or conveying. 397 | 398 | If you add terms to a covered work in accord with this section, you 399 | must place, in the relevant source files, a statement of the 400 | additional terms that apply to those files, or a notice indicating 401 | where to find the applicable terms. 402 | 403 | Additional terms, permissive or non-permissive, may be stated in the 404 | form of a separately written license, or stated as exceptions; 405 | the above requirements apply either way. 406 | 407 | 8. Termination. 408 | 409 | You may not propagate or modify a covered work except as expressly 410 | provided under this License. Any attempt otherwise to propagate or 411 | modify it is void, and will automatically terminate your rights under 412 | this License (including any patent licenses granted under the third 413 | paragraph of section 11). 414 | 415 | However, if you cease all violation of this License, then your 416 | license from a particular copyright holder is reinstated (a) 417 | provisionally, unless and until the copyright holder explicitly and 418 | finally terminates your license, and (b) permanently, if the copyright 419 | holder fails to notify you of the violation by some reasonable means 420 | prior to 60 days after the cessation. 421 | 422 | Moreover, your license from a particular copyright holder is 423 | reinstated permanently if the copyright holder notifies you of the 424 | violation by some reasonable means, this is the first time you have 425 | received notice of violation of this License (for any work) from that 426 | copyright holder, and you cure the violation prior to 30 days after 427 | your receipt of the notice. 428 | 429 | Termination of your rights under this section does not terminate the 430 | licenses of parties who have received copies or rights from you under 431 | this License. If your rights have been terminated and not permanently 432 | reinstated, you do not qualify to receive new licenses for the same 433 | material under section 10. 434 | 435 | 9. Acceptance Not Required for Having Copies. 436 | 437 | You are not required to accept this License in order to receive or 438 | run a copy of the Program. Ancillary propagation of a covered work 439 | occurring solely as a consequence of using peer-to-peer transmission 440 | to receive a copy likewise does not require acceptance. However, 441 | nothing other than this License grants you permission to propagate or 442 | modify any covered work. These actions infringe copyright if you do 443 | not accept this License. Therefore, by modifying or propagating a 444 | covered work, you indicate your acceptance of this License to do so. 445 | 446 | 10. Automatic Licensing of Downstream Recipients. 447 | 448 | Each time you convey a covered work, the recipient automatically 449 | receives a license from the original licensors, to run, modify and 450 | propagate that work, subject to this License. You are not responsible 451 | for enforcing compliance by third parties with this License. 452 | 453 | An "entity transaction" is a transaction transferring control of an 454 | organization, or substantially all assets of one, or subdividing an 455 | organization, or merging organizations. If propagation of a covered 456 | work results from an entity transaction, each party to that 457 | transaction who receives a copy of the work also receives whatever 458 | licenses to the work the party's predecessor in interest had or could 459 | give under the previous paragraph, plus a right to possession of the 460 | Corresponding Source of the work from the predecessor in interest, if 461 | the predecessor has it or can get it with reasonable efforts. 462 | 463 | You may not impose any further restrictions on the exercise of the 464 | rights granted or affirmed under this License. For example, you may 465 | not impose a license fee, royalty, or other charge for exercise of 466 | rights granted under this License, and you may not initiate litigation 467 | (including a cross-claim or counterclaim in a lawsuit) alleging that 468 | any patent claim is infringed by making, using, selling, offering for 469 | sale, or importing the Program or any portion of it. 470 | 471 | 11. Patents. 472 | 473 | A "contributor" is a copyright holder who authorizes use under this 474 | License of the Program or a work on which the Program is based. The 475 | work thus licensed is called the contributor's "contributor version". 476 | 477 | A contributor's "essential patent claims" are all patent claims 478 | owned or controlled by the contributor, whether already acquired or 479 | hereafter acquired, that would be infringed by some manner, permitted 480 | by this License, of making, using, or selling its contributor version, 481 | but do not include claims that would be infringed only as a 482 | consequence of further modification of the contributor version. For 483 | purposes of this definition, "control" includes the right to grant 484 | patent sublicenses in a manner consistent with the requirements of 485 | this License. 486 | 487 | Each contributor grants you a non-exclusive, worldwide, royalty-free 488 | patent license under the contributor's essential patent claims, to 489 | make, use, sell, offer for sale, import and otherwise run, modify and 490 | propagate the contents of its contributor version. 491 | 492 | In the following three paragraphs, a "patent license" is any express 493 | agreement or commitment, however denominated, not to enforce a patent 494 | (such as an express permission to practice a patent or covenant not to 495 | sue for patent infringement). To "grant" such a patent license to a 496 | party means to make such an agreement or commitment not to enforce a 497 | patent against the party. 498 | 499 | If you convey a covered work, knowingly relying on a patent license, 500 | and the Corresponding Source of the work is not available for anyone 501 | to copy, free of charge and under the terms of this License, through a 502 | publicly available network server or other readily accessible means, 503 | then you must either (1) cause the Corresponding Source to be so 504 | available, or (2) arrange to deprive yourself of the benefit of the 505 | patent license for this particular work, or (3) arrange, in a manner 506 | consistent with the requirements of this License, to extend the patent 507 | license to downstream recipients. "Knowingly relying" means you have 508 | actual knowledge that, but for the patent license, your conveying the 509 | covered work in a country, or your recipient's use of the covered work 510 | in a country, would infringe one or more identifiable patents in that 511 | country that you have reason to believe are valid. 512 | 513 | If, pursuant to or in connection with a single transaction or 514 | arrangement, you convey, or propagate by procuring conveyance of, a 515 | covered work, and grant a patent license to some of the parties 516 | receiving the covered work authorizing them to use, propagate, modify 517 | or convey a specific copy of the covered work, then the patent license 518 | you grant is automatically extended to all recipients of the covered 519 | work and works based on it. 520 | 521 | A patent license is "discriminatory" if it does not include within 522 | the scope of its coverage, prohibits the exercise of, or is 523 | conditioned on the non-exercise of one or more of the rights that are 524 | specifically granted under this License. You may not convey a covered 525 | work if you are a party to an arrangement with a third party that is 526 | in the business of distributing software, under which you make payment 527 | to the third party based on the extent of your activity of conveying 528 | the work, and under which the third party grants, to any of the 529 | parties who would receive the covered work from you, a discriminatory 530 | patent license (a) in connection with copies of the covered work 531 | conveyed by you (or copies made from those copies), or (b) primarily 532 | for and in connection with specific products or compilations that 533 | contain the covered work, unless you entered into that arrangement, 534 | or that patent license was granted, prior to 28 March 2007. 535 | 536 | Nothing in this License shall be construed as excluding or limiting 537 | any implied license or other defenses to infringement that may 538 | otherwise be available to you under applicable patent law. 539 | 540 | 12. No Surrender of Others' Freedom. 541 | 542 | If conditions are imposed on you (whether by court order, agreement or 543 | otherwise) that contradict the conditions of this License, they do not 544 | excuse you from the conditions of this License. If you cannot convey a 545 | covered work so as to satisfy simultaneously your obligations under this 546 | License and any other pertinent obligations, then as a consequence you may 547 | not convey it at all. For example, if you agree to terms that obligate you 548 | to collect a royalty for further conveying from those to whom you convey 549 | the Program, the only way you could satisfy both those terms and this 550 | License would be to refrain entirely from conveying the Program. 551 | 552 | 13. Use with the GNU Affero General Public License. 553 | 554 | Notwithstanding any other provision of this License, you have 555 | permission to link or combine any covered work with a work licensed 556 | under version 3 of the GNU Affero General Public License into a single 557 | combined work, and to convey the resulting work. The terms of this 558 | License will continue to apply to the part which is the covered work, 559 | but the special requirements of the GNU Affero General Public License, 560 | section 13, concerning interaction through a network will apply to the 561 | combination as such. 562 | 563 | 14. Revised Versions of this License. 564 | 565 | The Free Software Foundation may publish revised and/or new versions of 566 | the GNU General Public License from time to time. Such new versions will 567 | be similar in spirit to the present version, but may differ in detail to 568 | address new problems or concerns. 569 | 570 | Each version is given a distinguishing version number. If the 571 | Program specifies that a certain numbered version of the GNU General 572 | Public License "or any later version" applies to it, you have the 573 | option of following the terms and conditions either of that numbered 574 | version or of any later version published by the Free Software 575 | Foundation. If the Program does not specify a version number of the 576 | GNU General Public License, you may choose any version ever published 577 | by the Free Software Foundation. 578 | 579 | If the Program specifies that a proxy can decide which future 580 | versions of the GNU General Public License can be used, that proxy's 581 | public statement of acceptance of a version permanently authorizes you 582 | to choose that version for the Program. 583 | 584 | Later license versions may give you additional or different 585 | permissions. However, no additional obligations are imposed on any 586 | author or copyright holder as a result of your choosing to follow a 587 | later version. 588 | 589 | 15. Disclaimer of Warranty. 590 | 591 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 592 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 593 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 594 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 595 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 596 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 597 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 598 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 599 | 600 | 16. Limitation of Liability. 601 | 602 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 603 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 604 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 605 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 606 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 607 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 608 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 609 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 610 | SUCH DAMAGES. 611 | 612 | 17. Interpretation of Sections 15 and 16. 613 | 614 | If the disclaimer of warranty and limitation of liability provided 615 | above cannot be given local legal effect according to their terms, 616 | reviewing courts shall apply local law that most closely approximates 617 | an absolute waiver of all civil liability in connection with the 618 | Program, unless a warranty or assumption of liability accompanies a 619 | copy of the Program in return for a fee. 620 | 621 | END OF TERMS AND CONDITIONS 622 | 623 | How to Apply These Terms to Your New Programs 624 | 625 | If you develop a new program, and you want it to be of the greatest 626 | possible use to the public, the best way to achieve this is to make it 627 | free software which everyone can redistribute and change under these terms. 628 | 629 | To do so, attach the following notices to the program. It is safest 630 | to attach them to the start of each source file to most effectively 631 | state the exclusion of warranty; and each file should have at least 632 | the "copyright" line and a pointer to where the full notice is found. 633 | 634 | 635 | Copyright (C) 636 | 637 | This program is free software: you can redistribute it and/or modify 638 | it under the terms of the GNU General Public License as published by 639 | the Free Software Foundation, either version 3 of the License, or 640 | (at your option) any later version. 641 | 642 | This program is distributed in the hope that it will be useful, 643 | but WITHOUT ANY WARRANTY; without even the implied warranty of 644 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 645 | GNU General Public License for more details. 646 | 647 | You should have received a copy of the GNU General Public License 648 | along with this program. If not, see . 649 | 650 | Also add information on how to contact you by electronic and paper mail. 651 | 652 | If the program does terminal interaction, make it output a short 653 | notice like this when it starts in an interactive mode: 654 | 655 | Copyright (C) 656 | This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 657 | This is free software, and you are welcome to redistribute it 658 | under certain conditions; type `show c' for details. 659 | 660 | The hypothetical commands `show w' and `show c' should show the appropriate 661 | parts of the General Public License. Of course, your program's commands 662 | might be different; for a GUI interface, you would use an "about box". 663 | 664 | You should also get your employer (if you work as a programmer) or school, 665 | if any, to sign a "copyright disclaimer" for the program, if necessary. 666 | For more information on this, and how to apply and follow the GNU GPL, see 667 | . 668 | 669 | The GNU General Public License does not permit incorporating your program 670 | into proprietary programs. If your program is a subroutine library, you 671 | may consider it more useful to permit linking proprietary applications with 672 | the library. If this is what you want to do, use the GNU Lesser General 673 | Public License instead of this License. But first, please read 674 | . --------------------------------------------------------------------------------