Merge branch 'frost' into feature-remove-holes
This commit is contained in:
commit
4c2967cb93
76 changed files with 1186 additions and 3575 deletions
|
@ -4,7 +4,7 @@ FROM python:3.10 AS base
|
|||
|
||||
ARG DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
RUN apt update && apt -y upgrade && apt install -y supervisor ffmpeg
|
||||
RUN apt update && apt -y upgrade && apt install -y supervisor
|
||||
|
||||
# we'll end up blowing away this directory via docker-compose
|
||||
WORKDIR /service
|
||||
|
@ -13,7 +13,7 @@ COPY poetry.lock .
|
|||
RUN pip install 'poetry==1.2.2'
|
||||
RUN poetry config virtualenvs.create false && poetry install
|
||||
|
||||
RUN mkdir /images && mkdir /songs
|
||||
RUN mkdir /images
|
||||
|
||||
EXPOSE 80/tcp
|
||||
|
||||
|
@ -24,7 +24,7 @@ ENV FLASK_APP=files/cli:app
|
|||
# Release container
|
||||
FROM base AS release
|
||||
|
||||
COPY supervisord.conf.release /etc/supervisord.conf
|
||||
COPY bootstrap/supervisord.conf.release /etc/supervisord.conf
|
||||
CMD [ "/usr/bin/supervisord", "-c", "/etc/supervisord.conf" ]
|
||||
|
||||
|
||||
|
@ -36,7 +36,7 @@ FROM release AS dev
|
|||
COPY thirdparty/sqlalchemy-easy-profile sqlalchemy-easy-profile
|
||||
RUN cd sqlalchemy-easy-profile && python3 setup.py install
|
||||
|
||||
COPY supervisord.conf.dev /etc/supervisord.conf
|
||||
COPY bootstrap/supervisord.conf.dev /etc/supervisord.conf
|
||||
CMD [ "/usr/bin/supervisord", "-c", "/etc/supervisord.conf" ]
|
||||
|
||||
|
||||
|
|
|
@ -8,7 +8,6 @@ HCAPTCHA_SECRET=blahblahblah
|
|||
YOUTUBE_KEY=blahblahblah
|
||||
PUSHER_ID=blahblahblah
|
||||
PUSHER_KEY=blahblahblah
|
||||
IMGUR_KEY=blahblahblah
|
||||
SPAM_SIMILARITY_THRESHOLD=0.5
|
||||
SPAM_URL_SIMILARITY_THRESHOLD=0.1
|
||||
SPAM_SIMILAR_COUNT_THRESHOLD=10
|
||||
|
@ -31,6 +30,7 @@ MENTION_LIMIT=100
|
|||
MULTIMEDIA_EMBEDDING_ENABLED=False
|
||||
RESULTS_PER_PAGE_COMMENTS=200
|
||||
SCORE_HIDING_TIME_HOURS=24
|
||||
SQLALCHEMY_WARN_20=1
|
||||
|
||||
# Profiling system; uncomment to enable
|
||||
# Stores and exposes sensitive data!
|
|
@ -5,7 +5,7 @@ logfile=/tmp/supervisord.log
|
|||
|
||||
[program:service]
|
||||
directory=/service
|
||||
command=sh -c 'python3 -m flask db upgrade && ENABLE_SERVICES=true gunicorn files.__main__:app -k gevent -w $(( `nproc` * 2 )) --reload -b 0.0.0.0:80 --max-requests 1000 --max-requests-jitter 500'
|
||||
command=sh -c 'python3 -m flask db upgrade && ENABLE_SERVICES=true gunicorn files.__main__:app -k gevent -w ${CORE_OVERRIDE:-$(( `nproc` * 2 ))} --reload -b 0.0.0.0:80 --max-requests 1000 --max-requests-jitter 500'
|
||||
stdout_logfile=/dev/stdout
|
||||
stdout_logfile_maxbytes=0
|
||||
stderr_logfile=/dev/stderr
|
|
@ -1,7 +1,6 @@
|
|||
version: '2.3'
|
||||
|
||||
services:
|
||||
files:
|
||||
container_name: "themotte"
|
||||
site:
|
||||
build:
|
||||
target: operation
|
||||
|
|
|
@ -1,14 +1,13 @@
|
|||
version: '2.3'
|
||||
|
||||
services:
|
||||
files:
|
||||
container_name: "themotte"
|
||||
site:
|
||||
build:
|
||||
context: .
|
||||
target: dev
|
||||
volumes:
|
||||
- "./:/service"
|
||||
env_file: env
|
||||
env_file: bootstrap/site_env
|
||||
environment:
|
||||
- DATABASE_URL=postgresql://postgres@postgres:5432
|
||||
- REDIS_URL=redis://redis
|
||||
|
@ -23,19 +22,17 @@ services:
|
|||
- postgres
|
||||
|
||||
redis:
|
||||
container_name: "themotte_redis"
|
||||
image: redis
|
||||
ports:
|
||||
- "6379:6379"
|
||||
|
||||
postgres:
|
||||
container_name: "themotte_postgres"
|
||||
image: postgres:12.3
|
||||
# command: ["postgres", "-c", "log_statement=all"]
|
||||
# uncomment this if u wanna output all SQL queries to the console
|
||||
volumes:
|
||||
- "./schema.sql:/docker-entrypoint-initdb.d/00-schema.sql"
|
||||
- "./seed-db.sql:/docker-entrypoint-initdb.d/10-seed-db.sql"
|
||||
- "./bootstrap/original-schema.sql:/docker-entrypoint-initdb.d/00-schema.sql"
|
||||
- "./bootstrap/original-seed-db.sql:/docker-entrypoint-initdb.d/10-seed-db.sql"
|
||||
environment:
|
||||
- POSTGRES_HOST_AUTH_METHOD=trust
|
||||
ports:
|
||||
|
|
|
@ -141,8 +141,8 @@ app.config['RATE_LIMITER_ENABLED'] = not bool_from_string(environ.get('DBG_LIMIT
|
|||
if not app.config['RATE_LIMITER_ENABLED']:
|
||||
print("Rate limiter disabled in debug mode!")
|
||||
limiter = Limiter(
|
||||
app,
|
||||
key_func=get_remote_addr,
|
||||
app=app,
|
||||
default_limits=["3/second;30/minute;200/hour;1000/day"],
|
||||
application_limits=["10/second;200/minute;5000/hour;10000/day"],
|
||||
storage_uri=environ.get("REDIS_URL", "redis://localhost"),
|
||||
|
|
|
@ -1,7 +0,0 @@
|
|||
new BugController({
|
||||
imageSprite: "/assets/images/fly-sprite.webp",
|
||||
canDie: false,
|
||||
minBugs: 10,
|
||||
maxBugs: 20,
|
||||
mouseOver: "multiply"
|
||||
});
|
|
@ -1,33 +0,0 @@
|
|||
var BugDispatch={options:{minDelay:500,maxDelay:1E4,minBugs:2,maxBugs:20,minSpeed:5,maxSpeed:10,maxLargeTurnDeg:150,maxSmallTurnDeg:10,maxWiggleDeg:5,imageSprite:"fireflies.webp",bugWidth:13,bugHeight:14,num_frames:5,zoom:10,canFly:!0,canDie:!0,numDeathTypes:3,monitorMouseMovement:!1,eventDistanceToBug:40,minTimeBetweenMultipy:1E3,mouseOver:"random"},initialize:function(a){this.options=mergeOptions(this.options,a);this.options.minBugs>this.options.maxBugs&&(this.options.minBugs=this.options.maxBugs);
|
||||
this.modes=["multiply","nothing"];this.options.canFly&&this.modes.push("fly","flyoff");this.options.canDie&&this.modes.push("die");-1==this.modes.indexOf(this.options.mouseOver)&&(this.options.mouseOver="random");this.transform=null;this.transforms={Moz:function(a){this.bug.style.MozTransform=a},webkit:function(a){this.bug.style.webkitTransform=a},O:function(a){this.bug.style.OTransform=a},ms:function(a){this.bug.style.msTransform=a},Khtml:function(a){this.bug.style.KhtmlTransform=a},w3c:function(a){this.bug.style.transform=
|
||||
a}};if("transform"in document.documentElement.style)this.transform=this.transforms.w3c;else{var b=["Moz","webkit","O","ms","Khtml"],c=0;for(c=0;c<b.length;c++)if(b[c]+"Transform"in document.documentElement.style){this.transform=this.transforms[b[c]];break}}if(this.transform){this.bugs=[];b="multiply"===this.options.mouseOver?this.options.minBugs:this.random(this.options.minBugs,this.options.maxBugs,!0);c=0;var d=this;for(c=0;c<b;c++){a=JSON.parse(JSON.stringify(this.options));var e=SpawnBug();a.wingsOpen=
|
||||
this.options.canFly?.5<Math.random()?!0:!1:!0;a.walkSpeed=this.random(this.options.minSpeed,this.options.maxSpeed);e.initialize(this.transform,a);this.bugs.push(e)}this.spawnDelay=[];for(c=0;c<b;c++)a=this.random(this.options.minDelay,this.options.maxDelay,!0),e=this.bugs[c],this.spawnDelay[c]=setTimeout(function(a){return function(){d.options.canFly?a.flyIn():a.walkIn()}}(e),a),d.add_events_to_bug(e);this.options.monitorMouseMovement&&(window.onmousemove=function(){d.check_if_mouse_close_to_bug()})}},
|
||||
stop:function(){for(var a=0;a<this.bugs.length;a++)this.spawnDelay[a]&&clearTimeout(this.spawnDelay[a]),this.bugs[a].stop()},end:function(){for(var a=0;a<this.bugs.length;a++)this.spawnDelay[a]&&clearTimeout(this.spawnDelay[a]),this.bugs[a].stop(),this.bugs[a].remove()},reset:function(){this.stop();for(var a=0;a<this.bugs.length;a++)this.bugs[a].reset(),this.bugs[a].walkIn()},killAll:function(){for(var a=0;a<this.bugs.length;a++)this.spawnDelay[a]&&clearTimeout(this.spawnDelay[a]),this.bugs[a].die()},
|
||||
add_events_to_bug:function(a){var b=this;a.bug&&(a.bug.addEventListener?a.bug.addEventListener("mouseover",function(c){b.on_bug(a)}):a.bug.attachEvent&&a.bug.attachEvent("onmouseover",function(c){b.on_bug(a)}))},check_if_mouse_close_to_bug:function(a){if(a=a||window.event){var b=0,c=0;a.client&&a.client.x?(b=a.client.x,c=a.client.y):a.clientX?(b=a.clientX,c=a.clientY):a.page&&a.page.x?(b=a.page.x-(document.body.scrollLeft+document.documentElement.scrollLeft),c=a.page.y-(document.body.scrollTop+document.documentElement.scrollTop)):
|
||||
a.pageX&&(b=a.pageX-(document.body.scrollLeft+document.documentElement.scrollLeft),c=a.pageY-(document.body.scrollTop+document.documentElement.scrollTop));a=this.bugs.length;var d;for(d=0;d<a;d++){var e=this.bugs[d].getPos();e&&Math.abs(e.top-c)+Math.abs(e.left-b)<this.options.eventDistanceToBug&&!this.bugs[d].flyperiodical&&this.near_bug(this.bugs[d])}}},near_bug:function(a){this.on_bug(a)},on_bug:function(a){if(a.alive){var b=this.options.mouseOver;"random"===b&&(b=this.modes[this.random(0,this.modes.length-
|
||||
1,!0)]);if("fly"===b)a.stop(),a.flyRand();else if("nothing"!==b)if("flyoff"===b)a.stop(),a.flyOff();else if("die"===b)a.die();else if("multiply"===b&&!this.multiplyDelay&&this.bugs.length<this.options.maxBugs){var c=SpawnBug();b=JSON.parse(JSON.stringify(this.options));var d=a.getPos(),e=this;b.wingsOpen=this.options.canFly?.5<Math.random()?!0:!1:!0;b.walkSpeed=this.random(this.options.minSpeed,this.options.maxSpeed);c.initialize(this.transform,b);c.drawBug(d.top,d.left);b.canFly?(c.flyRand(),a.flyRand()):
|
||||
(c.go(),a.go());this.bugs.push(c);this.multiplyDelay=!0;setTimeout(function(){e.add_events_to_bug(c);e.multiplyDelay=!1},this.options.minTimeBetweenMultipy)}}},random:function(a,b,c){if(a==b)return c?Math.round(a):a;var d=a-.5+Math.random()*(b-a+1);d>b?d=b:d<a&&(d=a);return c?Math.round(d):d}},BugController=function(){this.initialize.apply(this,arguments)};BugController.prototype=BugDispatch;
|
||||
var SpiderController=function(){this.options=mergeOptions(this.options,{imageSprite:"spider-sprite.webp",bugWidth:69,bugHeight:90,num_frames:7,canFly:!1,canDie:!0,numDeathTypes:2,zoom:6,minDelay:200,maxDelay:3E3,minSpeed:6,maxSpeed:13,minBugs:3,maxBugs:10});this.initialize.apply(this,arguments)};SpiderController.prototype=BugDispatch;
|
||||
var Bug={options:{wingsOpen:!1,walkSpeed:2,flySpeed:40,edge_resistance:50,zoom:10},initialize:function(a,b){this.options=mergeOptions(this.options,b);this.NEAR_TOP_EDGE=1;this.NEAR_BOTTOM_EDGE=2;this.NEAR_LEFT_EDGE=4;this.NEAR_RIGHT_EDGE=8;this.directions={};this.directions[this.NEAR_TOP_EDGE]=270;this.directions[this.NEAR_BOTTOM_EDGE]=90;this.directions[this.NEAR_LEFT_EDGE]=0;this.directions[this.NEAR_RIGHT_EDGE]=180;this.directions[this.NEAR_TOP_EDGE+this.NEAR_LEFT_EDGE]=315;this.directions[this.NEAR_TOP_EDGE+
|
||||
this.NEAR_RIGHT_EDGE]=225;this.directions[this.NEAR_BOTTOM_EDGE+this.NEAR_LEFT_EDGE]=45;this.directions[this.NEAR_BOTTOM_EDGE+this.NEAR_RIGHT_EDGE]=135;this.large_turn_angle_deg=this.angle_rad=this.angle_deg=0;this.near_edge=!1;this.edge_test_counter=10;this.fly_counter=this.large_turn_counter=this.small_turn_counter=0;this.toggle_stationary_counter=50*Math.random();this.zoom=this.random(this.options.zoom,10)/10;this.stationary=!1;this.bug=null;this.active=!0;this.wingsOpen=this.options.wingsOpen;
|
||||
this.transform=a;this.flyIndex=this.walkIndex=0;this.alive=!0;this.twitchTimer=null;this.rad2deg_k=180/Math.PI;this.deg2rad_k=Math.PI/180;this.makeBug();this.angle_rad=this.deg2rad(this.angle_deg);this.angle_deg=this.random(0,360,!0)},go:function(){if(this.transform){this.drawBug();var a=this;this.animating=!0;this.going=requestAnimFrame(function(b){a.animate(b)})}},stop:function(){this.animating=!1;this.going&&(clearTimeout(this.going),this.going=null);this.flyperiodical&&(clearTimeout(this.flyperiodical),
|
||||
this.flyperiodical=null);this.twitchTimer&&(clearTimeout(this.twitchTimer),this.twitchTimer=null)},remove:function(){this.active=!1;this.inserted&&this.bug.parentNode&&(this.bug.parentNode.removeChild(this.bug),this.inserted=!1)},reset:function(){this.active=this.alive=!0;this.bug.style.bottom="";this.bug.style.top=0;this.bug.style.left=0;this.bug.classList.remove("bug-dead")},animate:function(a){if(this.animating&&this.alive&&this.active){var b=this;this.going=requestAnimFrame(function(a){b.animate(a)});
|
||||
"_lastTimestamp"in this||(this._lastTimestamp=a);var c=a-this._lastTimestamp;if(!(40>c||(200<c&&(c=200),this._lastTimestamp=a,0>=--this.toggle_stationary_counter&&this.toggleStationary(),this.stationary))){if(0>=--this.edge_test_counter&&this.bug_near_window_edge()&&(this.angle_deg%=360,0>this.angle_deg&&(this.angle_deg+=360),15<Math.abs(this.directions[this.near_edge]-this.angle_deg))){a=this.directions[this.near_edge]-this.angle_deg;var d=360-this.angle_deg+this.directions[this.near_edge];this.large_turn_angle_deg=
|
||||
Math.abs(a)<Math.abs(d)?a:d;this.edge_test_counter=10;this.large_turn_counter=100;this.small_turn_counter=30}0>=--this.large_turn_counter&&(this.large_turn_angle_deg=this.random(1,this.options.maxLargeTurnDeg,!0),this.next_large_turn());if(0>=--this.small_turn_counter)this.angle_deg+=this.random(1,this.options.maxSmallTurnDeg),this.next_small_turn();else{a=this.random(1,this.options.maxWiggleDeg,!0);if(0<this.large_turn_angle_deg&&0>a||0>this.large_turn_angle_deg&&0<a)a=-a;this.large_turn_angle_deg-=
|
||||
a;this.angle_deg+=a}this.angle_rad=this.deg2rad(this.angle_deg);this.moveBug(this.bug.left+c/100*this.options.walkSpeed*Math.cos(this.angle_rad),this.bug.top+c/100*this.options.walkSpeed*-Math.sin(this.angle_rad),90-this.angle_deg);this.walkFrame()}}},makeBug:function(){if(!this.bug&&this.active){var a=this.wingsOpen?"0":"-"+this.options.bugHeight+"px",b=document.createElement("div");b.className="bug";b.style.background="transparent url("+this.options.imageSprite+") no-repeat 0 "+a;b.style.width=
|
||||
this.options.bugWidth+"px";b.style.height=this.options.bugHeight+"px";b.style.position="fixed";b.style.top=0;b.style.left=0;b.style.zIndex="9999999";this.bug=b;this.setPos()}},setPos:function(a,b){this.bug.top=a||this.random(this.options.edge_resistance,document.documentElement.clientHeight-this.options.edge_resistance);this.bug.left=b||this.random(this.options.edge_resistance,document.documentElement.clientWidth-this.options.edge_resistance);this.moveBug(this.bug.left,this.bug.top,90-this.angle_deg)},
|
||||
moveBug:function(a,b,c){this.bug.left=a;this.bug.top=b;a="translate("+parseInt(a)+"px,"+parseInt(b)+"px)";c&&(a+=" rotate("+c+"deg)");a+=" scale("+this.zoom+")";this.transform(a)},drawBug:function(a,b){this.bug||this.makeBug();this.bug&&(a&&b?this.setPos(a,b):this.setPos(this.bug.top,this.bug.left),this.inserted||(this.inserted=!0,document.body.appendChild(this.bug)))},toggleStationary:function(){this.stationary=!this.stationary;this.next_stationary();var a=this.wingsOpen?"0":"-"+this.options.bugHeight+
|
||||
"px";this.bug.style.backgroundPosition=this.stationary?"0 "+a:"-"+this.options.bugWidth+"px "+a},walkFrame:function(){this.bug.style.backgroundPosition=-1*this.walkIndex*this.options.bugWidth+"px "+(this.wingsOpen?"0":"-"+this.options.bugHeight+"px");this.walkIndex++;this.walkIndex>=this.options.num_frames&&(this.walkIndex=0)},fly:function(a){var b=this.bug.top,c=this.bug.left,d=c-a.left,e=b-a.top,f=Math.atan(e/d);50>Math.abs(d)+Math.abs(e)&&(this.bug.style.backgroundPosition=-2*this.options.bugWidth+
|
||||
"px -"+2*this.options.bugHeight+"px");30>Math.abs(d)+Math.abs(e)&&(this.bug.style.backgroundPosition=-1*this.options.bugWidth+"px -"+2*this.options.bugHeight+"px");if(10>Math.abs(d)+Math.abs(e))this.bug.style.backgroundPosition="0 0",this.stop(),this.go();else{var g=Math.cos(f)*this.options.flySpeed;f=Math.sin(f)*this.options.flySpeed;if(c>a.left&&0<g||c>a.left&&0>g)g*=-1,Math.abs(d)<Math.abs(g)&&(g/=4);if(b<a.top&&0>f||b>a.top&&0<f)f*=-1,Math.abs(e)<Math.abs(f)&&(f/=4);this.moveBug(c+g,b+f)}},flyRand:function(){this.stop();
|
||||
var a={};a.top=this.random(this.options.edge_resistance,document.documentElement.clientHeight-this.options.edge_resistance);a.left=this.random(this.options.edge_resistance,document.documentElement.clientWidth-this.options.edge_resistance);this.startFlying(a)},startFlying:function(a){var b=this.bug.top,c=this.bug.left,d=a.left-c,e=a.top-b;this.bug.left=a.left;this.bug.top=a.top;this.angle_rad=Math.atan(e/d);this.angle_deg=this.rad2deg(this.angle_rad);this.angle_deg=0<d?90+this.angle_deg:270+this.angle_deg;
|
||||
this.moveBug(c,b,this.angle_deg);var f=this;this.flyperiodical=setInterval(function(){f.fly(a)},10)},flyIn:function(){this.bug||this.makeBug();if(this.bug){this.stop();var a=Math.round(4*Math.random()-.5),b=document,c=b.documentElement,d=b.getElementsByTagName("body")[0];b=window.innerWidth||c.clientWidth||d.clientWidth;c=window.innerHeight||c.clientHeight||d.clientHeight;3<a&&(a=3);0>a&&(a=0);0===a?(a=-2*this.options.bugHeight,b*=Math.random()):1===a?(a=Math.random()*c,b+=2*this.options.bugWidth):
|
||||
2===a?(a=c+2*this.options.bugHeight,b*=Math.random()):(a=Math.random()*c,b=-3*this.options.bugWidth);this.bug.style.backgroundPosition=-3*this.options.bugWidth+"px "+(this.wingsOpen?"0":"-"+this.options.bugHeight+"px");this.bug.top=a;this.bug.left=b;this.drawBug();a={};a.top=this.random(this.options.edge_resistance,document.documentElement.clientHeight-this.options.edge_resistance);a.left=this.random(this.options.edge_resistance,document.documentElement.clientWidth-this.options.edge_resistance);this.startFlying(a)}},
|
||||
walkIn:function(){this.bug||this.makeBug();if(this.bug){this.stop();var a=Math.round(4*Math.random()-.5),b=document,c=b.documentElement,d=b.getElementsByTagName("body")[0];b=window.innerWidth||c.clientWidth||d.clientWidth;c=window.innerHeight||c.clientHeight||d.clientHeight;3<a&&(a=3);0>a&&(a=0);0===a?(a=-1.3*this.options.bugHeight,b*=Math.random()):1===a?(a=Math.random()*c,b+=.3*this.options.bugWidth):2===a?(a=c+.3*this.options.bugHeight,b*=Math.random()):(a=Math.random()*c,b=-1.3*this.options.bugWidth);
|
||||
this.bug.style.backgroundPosition=-3*this.options.bugWidth+"px "+(this.wingsOpen?"0":"-"+this.options.bugHeight+"px");this.bug.top=a;this.bug.left=b;this.drawBug();this.go()}},flyOff:function(){this.stop();var a=this.random(0,3),b={},c=document,d=c.documentElement,e=c.getElementsByTagName("body")[0];c=window.innerWidth||d.clientWidth||e.clientWidth;d=window.innerHeight||d.clientHeight||e.clientHeight;0===a?(b.top=-200,b.left=Math.random()*c):1===a?(b.top=Math.random()*d,b.left=c+200):2===a?(b.top=
|
||||
d+200,b.left=Math.random()*c):(b.top=Math.random()*d,b.left=-200);this.startFlying(b)},die:function(){this.stop();var a=this.random(0,this.options.numDeathTypes-1);this.alive=!1;this.drop(a)},drop:function(a){var b=this.bug.top,c=document,d=c.documentElement;c=c.getElementsByTagName("body")[0];var e=window.innerHeight||d.clientHeight||c.clientHeight;e-=this.options.bugHeight;var f=this.random(0,20,!0);Date.now();var g=this;this.bug.classList.add("bug-dead");this.dropTimer=requestAnimFrame(function(c){g._lastTimestamp=
|
||||
c;g.dropping(c,b,e,f,a)})},dropping:function(a,b,c,d,e){a-=this._lastTimestamp;var f=b+.002*a*a,g=this;f>=c?(f=c,clearTimeout(this.dropTimer),this.angle_deg=0,this.angle_rad=this.deg2rad(this.angle_deg),this.transform("rotate("+(90-this.angle_deg)+"deg) scale("+this.zoom+")"),this.bug.style.top=null,this.bug.style.bottom=Math.ceil((this.options.bugWidth*this.zoom-this.options.bugHeight*this.zoom)/2-this.options.bugHeight/2*(1-this.zoom))+"px",this.bug.style.left=this.bug.left+"px",this.bug.style.backgroundPosition=
|
||||
"-"+2*e*this.options.bugWidth+"px 100%",this.twitch(e)):(this.dropTimer=requestAnimFrame(function(a){g.dropping(a,b,c,d,e)}),20>a||(this.angle_deg=(this.angle_deg+d)%360,this.angle_rad=this.deg2rad(this.angle_deg),this.moveBug(this.bug.left,f,this.angle_deg)))},twitch:function(a,b){b||(b=0);var c=this;if(0===a||1===a)c.twitchTimer=setTimeout(function(){c.bug.style.backgroundPosition="-"+(2*a+b%2)*c.options.bugWidth+"px 100%";c.twitchTimer=setTimeout(function(){b++;c.bug.style.backgroundPosition="-"+
|
||||
(2*a+b%2)*c.options.bugWidth+"px 100%";c.twitch(a,++b)},c.random(300,800))},this.random(1E3,1E4))},rad2deg:function(a){return a*this.rad2deg_k},deg2rad:function(a){return a*this.deg2rad_k},random:function(a,b,c){if(a==b)return a;a=Math.round(a-.5+Math.random()*(b-a+1));return c?.5<Math.random()?a:-a:a},next_small_turn:function(){this.small_turn_counter=Math.round(10*Math.random())},next_large_turn:function(){this.large_turn_counter=Math.round(40*Math.random())},next_stationary:function(){this.toggle_stationary_counter=
|
||||
this.random(50,300)},bug_near_window_edge:function(){this.near_edge=0;this.bug.top<this.options.edge_resistance?this.near_edge|=this.NEAR_TOP_EDGE:this.bug.top>document.documentElement.clientHeight-this.options.edge_resistance&&(this.near_edge|=this.NEAR_BOTTOM_EDGE);this.bug.left<this.options.edge_resistance?this.near_edge|=this.NEAR_LEFT_EDGE:this.bug.left>document.documentElement.clientWidth-this.options.edge_resistance&&(this.near_edge|=this.NEAR_RIGHT_EDGE);return this.near_edge},getPos:function(){return this.inserted&&
|
||||
this.bug&&this.bug.style?{top:parseInt(this.bug.top,10),left:parseInt(this.bug.left,10)}:null}},SpawnBug=function(){var a={},b;for(b in Bug)Bug.hasOwnProperty(b)&&(a[b]=Bug[b]);return a},mergeOptions=function(a,b,c){"undefined"==typeof c&&(c=!0);a=c?cloneOf(a):a;for(var d in b)b.hasOwnProperty(d)&&(a[d]=b[d]);return a},cloneOf=function(a){if(null==a||"object"!=typeof a)return a;var b=a.constructor(),c;for(c in a)a.hasOwnProperty(c)&&(b[c]=cloneOf(a[c]));return b};
|
||||
window.requestAnimFrame=function(){return window.requestAnimationFrame||window.webkitRequestAnimationFrame||window.mozRequestAnimationFrame||window.oRequestAnimationFrame||window.msRequestAnimationFrame||function(a,b){window.setTimeout(a,1E3/60)}}();
|
|
@ -1,7 +0,0 @@
|
|||
new BugController({
|
||||
imageSprite: "/assets/images/fireflies.webp",
|
||||
canDie: false,
|
||||
minBugs: 10,
|
||||
maxBugs: 30,
|
||||
mouseOver: "multiply"
|
||||
});
|
|
@ -1,53 +0,0 @@
|
|||
let u_username = document.getElementById('u_username')
|
||||
|
||||
if (u_username)
|
||||
{
|
||||
u_username = u_username.innerHTML
|
||||
|
||||
let audio = new Audio(`/@${u_username}/song`);
|
||||
audio.loop=true;
|
||||
|
||||
function toggle() {
|
||||
if (audio.paused) audio.play()
|
||||
else audio.pause()
|
||||
}
|
||||
|
||||
audio.play();
|
||||
document.getElementById('userpage').addEventListener('click', () => {
|
||||
if (audio.paused) audio.play();
|
||||
}, {once : true});
|
||||
}
|
||||
else
|
||||
{
|
||||
let v_username = document.getElementById('v_username')
|
||||
if (v_username)
|
||||
{
|
||||
v_username = v_username.innerHTML
|
||||
|
||||
const paused = localStorage.getItem("paused")
|
||||
|
||||
let audio = new Audio(`/@${v_username}/song`);
|
||||
audio.loop=true;
|
||||
|
||||
function toggle() {
|
||||
if (audio.paused)
|
||||
{
|
||||
audio.play()
|
||||
localStorage.setItem("paused", "")
|
||||
}
|
||||
else
|
||||
{
|
||||
audio.pause()
|
||||
localStorage.setItem("paused", "1")
|
||||
}
|
||||
}
|
||||
|
||||
if (!paused)
|
||||
{
|
||||
audio.play();
|
||||
window.addEventListener('click', () => {
|
||||
if (audio.paused) audio.play();
|
||||
}, {once : true});
|
||||
}
|
||||
}
|
||||
}
|
|
@ -82,7 +82,7 @@ from .volunteer_janitor import VolunteerJanitorRecord
|
|||
|
||||
# Then the import * from files.*
|
||||
from files.helpers.const import *
|
||||
from files.helpers.images import *
|
||||
from files.helpers.media import *
|
||||
from files.helpers.lazy import *
|
||||
from files.helpers.security import *
|
||||
|
||||
|
|
|
@ -16,7 +16,7 @@ class OauthApp(Base):
|
|||
)
|
||||
|
||||
id = Column(Integer, primary_key=True)
|
||||
client_id = Column(String)
|
||||
client_id = Column(String(length=64))
|
||||
app_name = Column(String(length=50), nullable=False)
|
||||
redirect_uri = Column(String(length=50), nullable=False)
|
||||
description = Column(String(length=256), nullable=False)
|
||||
|
@ -35,7 +35,7 @@ class OauthApp(Base):
|
|||
@property
|
||||
@lazy
|
||||
def created_datetime(self):
|
||||
return str(time.strftime("%d/%B/%Y %H:%M:%S UTC", time.gmtime(self.created_utc)))
|
||||
return time.strftime("%d/%B/%Y %H:%M:%S UTC", time.gmtime(self.created_utc))
|
||||
|
||||
@property
|
||||
@lazy
|
||||
|
@ -74,7 +74,7 @@ class ClientAuth(Base):
|
|||
|
||||
user_id = Column(Integer, ForeignKey("users.id"), primary_key=True)
|
||||
oauth_client = Column(Integer, ForeignKey("oauth_apps.id"), primary_key=True)
|
||||
access_token = Column(String, nullable=False)
|
||||
access_token = Column(String(128), nullable=False)
|
||||
|
||||
user = relationship("User", viewonly=True)
|
||||
application = relationship("OauthApp", viewonly=True)
|
||||
|
@ -87,4 +87,4 @@ class ClientAuth(Base):
|
|||
@property
|
||||
@lazy
|
||||
def created_datetime(self):
|
||||
return str(time.strftime("%d/%B/%Y %H:%M:%S UTC", time.gmtime(self.created_utc)))
|
||||
return time.strftime("%d/%B/%Y %H:%M:%S UTC", time.gmtime(self.created_utc))
|
||||
|
|
|
@ -109,7 +109,7 @@ class Comment(Base):
|
|||
@property
|
||||
@lazy
|
||||
def created_datetime(self):
|
||||
return str(time.strftime("%d/%B/%Y %H:%M:%S UTC", time.gmtime(self.created_utc)))
|
||||
return time.strftime("%d/%B/%Y %H:%M:%S UTC", time.gmtime(self.created_utc))
|
||||
|
||||
@property
|
||||
@lazy
|
||||
|
@ -194,12 +194,9 @@ class Comment(Base):
|
|||
@property
|
||||
@lazy
|
||||
def parent(self):
|
||||
|
||||
if not self.parent_submission: return None
|
||||
|
||||
if self.level == 1: return self.post
|
||||
|
||||
else: return g.db.query(Comment).get(self.parent_comment_id)
|
||||
else: return g.db.get(Comment, self.parent_comment_id)
|
||||
|
||||
@property
|
||||
@lazy
|
||||
|
@ -295,13 +292,14 @@ class Comment(Base):
|
|||
return data
|
||||
|
||||
def award_count(self, kind):
|
||||
if not FEATURES['AWARDS']: return 0
|
||||
return len([x for x in self.awards if x.kind == kind])
|
||||
|
||||
@property
|
||||
@lazy
|
||||
def json_core(self):
|
||||
if self.is_banned:
|
||||
data= {'is_banned': True,
|
||||
data = {'is_banned': True,
|
||||
'ban_reason': self.ban_reason,
|
||||
'id': self.id,
|
||||
'post': self.post.id if self.post else 0,
|
||||
|
@ -309,38 +307,27 @@ class Comment(Base):
|
|||
'parent': self.parent_fullname
|
||||
}
|
||||
elif self.deleted_utc:
|
||||
data= {'deleted_utc': self.deleted_utc,
|
||||
data = {'deleted_utc': self.deleted_utc,
|
||||
'id': self.id,
|
||||
'post': self.post.id if self.post else 0,
|
||||
'level': self.level,
|
||||
'parent': self.parent_fullname
|
||||
}
|
||||
else:
|
||||
data = self.json_raw
|
||||
if self.level >= 2: data['parent_comment_id']= self.parent_comment_id
|
||||
|
||||
data=self.json_raw
|
||||
|
||||
if self.level>=2: data['parent_comment_id']= self.parent_comment_id
|
||||
|
||||
data['replies']=[x.json_core for x in self.replies(None)]
|
||||
data['replies'] = [x.json_core for x in self.replies(None)]
|
||||
|
||||
return data
|
||||
|
||||
@property
|
||||
@lazy
|
||||
def json(self):
|
||||
|
||||
data=self.json_core
|
||||
|
||||
if self.deleted_utc or self.is_banned:
|
||||
return data
|
||||
|
||||
data["author"]='👻' if self.ghost else self.author.json_core
|
||||
data["post"]=self.post.json_core if self.post else ''
|
||||
|
||||
if self.level >= 2:
|
||||
data["parent"]=self.parent.json_core
|
||||
|
||||
|
||||
data = self.json_core
|
||||
if self.deleted_utc or self.is_banned: return data
|
||||
data["author"] = '👻' if self.ghost else self.author.json_core
|
||||
data["post"] = self.post.json_core if self.post else ''
|
||||
return data
|
||||
|
||||
def realbody(self, v):
|
||||
|
@ -385,17 +372,10 @@ class Comment(Base):
|
|||
|
||||
def plainbody(self, v):
|
||||
if self.post and self.post.club and not (v and (v.paid_dues or v.id in [self.author_id, self.post.author_id])): return f"<p>{CC} ONLY</p>"
|
||||
|
||||
body = self.body
|
||||
|
||||
if not body: return ""
|
||||
|
||||
return body
|
||||
|
||||
def print(self):
|
||||
print(f'post: {self.id}, comment: {self.author_id}', flush=True)
|
||||
return ''
|
||||
|
||||
@lazy
|
||||
def collapse_for_user(self, v, path):
|
||||
if v and self.author_id == v.id: return False
|
||||
|
|
|
@ -34,7 +34,7 @@ class Flag(Base):
|
|||
@property
|
||||
@lazy
|
||||
def created_datetime(self):
|
||||
return str(time.strftime("%d/%B/%Y %H:%M:%S UTC", time.gmtime(self.created_utc)))
|
||||
return time.strftime("%d/%B/%Y %H:%M:%S UTC", time.gmtime(self.created_utc))
|
||||
|
||||
@lazy
|
||||
def realreason(self, v):
|
||||
|
@ -70,7 +70,7 @@ class CommentFlag(Base):
|
|||
@property
|
||||
@lazy
|
||||
def created_datetime(self):
|
||||
return str(time.strftime("%d/%B/%Y %H:%M:%S UTC", time.gmtime(self.created_utc)))
|
||||
return time.strftime("%d/%B/%Y %H:%M:%S UTC", time.gmtime(self.created_utc))
|
||||
|
||||
@lazy
|
||||
def realreason(self, v):
|
||||
|
|
252
files/classes/leaderboard.py
Normal file
252
files/classes/leaderboard.py
Normal file
|
@ -0,0 +1,252 @@
|
|||
from dataclasses import dataclass
|
||||
from typing import Any, Callable, Final, Optional
|
||||
|
||||
from sqlalchemy import Column, func
|
||||
from sqlalchemy.orm import scoped_session, Query
|
||||
|
||||
from files.helpers.const import LEADERBOARD_LIMIT
|
||||
|
||||
from files.classes.badges import Badge
|
||||
from files.classes.marsey import Marsey
|
||||
from files.classes.user import User
|
||||
from files.classes.userblock import UserBlock
|
||||
from files.helpers.get import get_accounts_dict
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class LeaderboardMeta:
|
||||
header_name:str
|
||||
table_header_name:str
|
||||
html_id:str
|
||||
table_column_name:str
|
||||
user_relative_url:Optional[str]
|
||||
limit:int=LEADERBOARD_LIMIT
|
||||
|
||||
class Leaderboard:
|
||||
def __init__(self, v:Optional[User], meta:LeaderboardMeta) -> None:
|
||||
self.v:Optional[User] = v
|
||||
self.meta:LeaderboardMeta = meta
|
||||
|
||||
@property
|
||||
def all_users(self) -> list[User]:
|
||||
raise NotImplementedError()
|
||||
|
||||
@property
|
||||
def v_position(self) -> Optional[int]:
|
||||
raise NotImplementedError()
|
||||
|
||||
@property
|
||||
def v_value(self) -> Optional[int]:
|
||||
raise NotImplementedError()
|
||||
|
||||
@property
|
||||
def v_appears_in_ranking(self) -> bool:
|
||||
return self.v_position is not None and self.v_position <= len(self.all_users)
|
||||
|
||||
@property
|
||||
def user_func(self) -> Callable[[Any], User]:
|
||||
return lambda u:u
|
||||
|
||||
@property
|
||||
def value_func(self) -> Callable[[User], int]:
|
||||
raise NotImplementedError()
|
||||
|
||||
class SimpleLeaderboard(Leaderboard):
|
||||
def __init__(self, v:User, meta:LeaderboardMeta, db:scoped_session, users_query:Query, column:Column):
|
||||
super().__init__(v, meta)
|
||||
self.db:scoped_session = db
|
||||
self.users_query:Query = users_query
|
||||
self.column:Column = column
|
||||
self._calculate()
|
||||
|
||||
def _calculate(self) -> None:
|
||||
self._all_users = self.users_query.order_by(self.column.desc()).limit(self.meta.limit).all()
|
||||
if self.v not in self._all_users:
|
||||
sq = self.db.query(User.id, self.column, func.rank().over(order_by=self.column.desc()).label("rank")).subquery()
|
||||
sq_data = self.db.query(sq.c.id, sq.c.column, sq.c.rank).filter(sq.c.id == self.v.id).limit(1).one()
|
||||
self._v_value:int = sq_data[1]
|
||||
self._v_position:int = sq_data[2]
|
||||
|
||||
@property
|
||||
def all_users(self) -> list[User]:
|
||||
return self._all_users
|
||||
|
||||
@property
|
||||
def v_position(self) -> int:
|
||||
return self._v_position
|
||||
|
||||
@property
|
||||
def v_value(self) -> int:
|
||||
return self._v_value
|
||||
|
||||
@property
|
||||
def value_func(self) -> Callable[[User], int]:
|
||||
return lambda u:getattr(u, self.column.name)
|
||||
|
||||
class _CountedAndRankedLeaderboard(Leaderboard):
|
||||
@classmethod
|
||||
def count_and_label(cls, criteria):
|
||||
return func.count(criteria).label("count")
|
||||
|
||||
@classmethod
|
||||
def rank_filtered_rank_label_by_desc(cls, criteria):
|
||||
return func.rank().over(order_by=func.count(criteria).desc()).label("rank")
|
||||
|
||||
class BadgeMarseyLeaderboard(_CountedAndRankedLeaderboard):
|
||||
def __init__(self, v:User, meta:LeaderboardMeta, db:scoped_session, column:Column):
|
||||
super().__init__(v, meta)
|
||||
self.db:scoped_session = db
|
||||
self.column = column
|
||||
self._calculate()
|
||||
|
||||
def _calculate(self):
|
||||
sq = self.db.query(self.column, self.count_and_label(self.column), self.rank_filtered_rank_label_by_desc(self.column)).group_by(self.column).subquery()
|
||||
sq_criteria = None
|
||||
if self.column == Badge.user_id:
|
||||
sq_criteria = User.id == sq.c.user_id
|
||||
elif self.column == Marsey.author_id:
|
||||
sq_criteria = User.id == sq.c.author_id
|
||||
else:
|
||||
raise ValueError("This leaderboard function only supports Badge.user_id and Marsey.author_id")
|
||||
leaderboard = self.db.query(User, sq.c.count).join(sq, sq_criteria).order_by(sq.c.count.desc())
|
||||
|
||||
position:Optional[tuple[int, int, int]] = self.db.query(User.id, sq.c.rank, sq.c.count).join(sq, sq_criteria).filter(User.id == self.v.id).one_or_none()
|
||||
if position and position[1]:
|
||||
self._v_position = position[1]
|
||||
self._v_value = position[2]
|
||||
else:
|
||||
self._v_position = leaderboard.count() + 1
|
||||
self._v_value = 0
|
||||
self._all_users = {k:v for k, v in leaderboard.limit(self.meta.limit).all()}
|
||||
|
||||
@property
|
||||
def all_users(self) -> list[User]:
|
||||
return list(self._all_users.keys())
|
||||
|
||||
@property
|
||||
def v_position(self) -> int:
|
||||
return self._v_position
|
||||
|
||||
@property
|
||||
def v_value(self) -> int:
|
||||
return self._v_value
|
||||
|
||||
@property
|
||||
def value_func(self) -> Callable[[User], int]:
|
||||
return lambda u:self._all_users[u]
|
||||
|
||||
class UserBlockLeaderboard(_CountedAndRankedLeaderboard):
|
||||
def __init__(self, v:User, meta:LeaderboardMeta, db:scoped_session, column:Column):
|
||||
super().__init__(v, meta)
|
||||
self.db:scoped_session = db
|
||||
self.column = column
|
||||
self._calculate()
|
||||
|
||||
def _calculate(self):
|
||||
if self.column != UserBlock.target_id:
|
||||
raise ValueError("This leaderboard function only supports UserBlock.target_id")
|
||||
sq = self.db.query(self.column, self.count_and_label(self.column)).group_by(self.column).subquery()
|
||||
leaderboard = self.db.query(User, sq.c.count).join(User, User.id == sq.c.target_id).order_by(sq.c.count.desc())
|
||||
|
||||
sq = self.db.query(self.column, self.count_and_label(self.column), self.rank_filtered_rank_label_by_desc(self.column)).group_by(self.column).subquery()
|
||||
position = self.db.query(sq.c.rank, sq.c.count).join(User, User.id == sq.c.target_id).filter(sq.c.target_id == self.v.id).limit(1).one_or_none()
|
||||
if not position: position = (leaderboard.count() + 1, 0)
|
||||
leaderboard = leaderboard.limit(self.meta.limit).all()
|
||||
self._all_users = {k:v for k, v in leaderboard}
|
||||
self._v_position = position[0]
|
||||
self._v_value = position[1]
|
||||
return (leaderboard, position[0], position[1])
|
||||
|
||||
@property
|
||||
def all_users(self) -> list[User]:
|
||||
return list(self._all_users.keys())
|
||||
|
||||
@property
|
||||
def v_position(self) -> int:
|
||||
return self._v_position
|
||||
|
||||
@property
|
||||
def v_value(self) -> int:
|
||||
return self._v_value
|
||||
|
||||
class RawSqlLeaderboard(Leaderboard):
|
||||
def __init__(self, meta:LeaderboardMeta, db:scoped_session, query:str) -> None: # should be LiteralString on py3.11+
|
||||
super().__init__(None, meta)
|
||||
self.db = db
|
||||
self._calculate(query)
|
||||
|
||||
def _calculate(self, query:str):
|
||||
self.result = {result[0]:list(result) for result in self.db.execute(query).all()}
|
||||
users = get_accounts_dict(self.result.keys(), db=self.db)
|
||||
if users is None:
|
||||
raise Exception("Some users don't exist when they should (was a user deleted?)")
|
||||
for user in users: # I know.
|
||||
self.result[user].append(users[user])
|
||||
|
||||
@property
|
||||
def all_users(self) -> list[User]:
|
||||
return [result[2] for result in self.result.values()]
|
||||
|
||||
@property
|
||||
def v_position(self) -> Optional[int]:
|
||||
return None
|
||||
|
||||
@property
|
||||
def v_value(self) -> Optional[int]:
|
||||
return None
|
||||
|
||||
@property
|
||||
def v_appears_in_ranking(self) -> bool:
|
||||
return True # we set this to True here to try and not grab the data
|
||||
|
||||
@property
|
||||
def user_func(self) -> Callable[[Any], User]:
|
||||
return lambda u:u
|
||||
|
||||
@property
|
||||
def value_func(self) -> Callable[[User], int]:
|
||||
return lambda u:self.result[u.id][1]
|
||||
|
||||
class ReceivedDownvotesLeaderboard(RawSqlLeaderboard):
|
||||
_query: Final[str] = """
|
||||
WITH cv_for_user AS (
|
||||
SELECT
|
||||
comments.author_id AS target_id,
|
||||
COUNT(*)
|
||||
FROM commentvotes cv
|
||||
JOIN comments ON comments.id = cv.comment_id
|
||||
WHERE vote_type = -1
|
||||
GROUP BY comments.author_id
|
||||
), sv_for_user AS (
|
||||
SELECT
|
||||
submissions.author_id AS target_id,
|
||||
COUNT(*)
|
||||
FROM votes sv
|
||||
JOIN submissions ON submissions.id = sv.submission_id
|
||||
WHERE vote_type = -1
|
||||
GROUP BY submissions.author_id
|
||||
)
|
||||
SELECT
|
||||
COALESCE(cvfu.target_id, svfu.target_id) AS target_id,
|
||||
(COALESCE(cvfu.count, 0) + COALESCE(svfu.count, 0)) AS count
|
||||
FROM cv_for_user cvfu
|
||||
FULL OUTER JOIN sv_for_user svfu
|
||||
ON cvfu.target_id = svfu.target_id
|
||||
ORDER BY count DESC LIMIT 25
|
||||
"""
|
||||
|
||||
def __init__(self, meta:LeaderboardMeta, db:scoped_session) -> None:
|
||||
super().__init__(meta, db, self._query)
|
||||
|
||||
class GivenUpvotesLeaderboard(RawSqlLeaderboard):
|
||||
_query: Final[str] = """
|
||||
SELECT
|
||||
COALESCE(cvbu.user_id, svbu.user_id) AS user_id,
|
||||
(COALESCE(cvbu.count, 0) + COALESCE(svbu.count, 0)) AS count
|
||||
FROM (SELECT user_id, COUNT(*) FROM votes WHERE vote_type = 1 GROUP BY user_id) AS svbu
|
||||
FULL OUTER JOIN (SELECT user_id, COUNT(*) FROM commentvotes WHERE vote_type = 1 GROUP BY user_id) AS cvbu
|
||||
ON cvbu.user_id = svbu.user_id
|
||||
ORDER BY count DESC LIMIT 25
|
||||
"""
|
||||
|
||||
def __init__(self, meta:LeaderboardMeta, db:scoped_session) -> None:
|
||||
super().__init__(meta, db, self._query)
|
|
@ -106,12 +106,12 @@ class Submission(Base):
|
|||
@property
|
||||
@lazy
|
||||
def created_datetime(self):
|
||||
return str(time.strftime("%d/%B/%Y %H:%M:%S UTC", time.gmtime(self.created_utc)))
|
||||
return time.strftime("%d/%B/%Y %H:%M:%S UTC", time.gmtime(self.created_utc))
|
||||
|
||||
@property
|
||||
@lazy
|
||||
def created_datetime(self):
|
||||
return str(time.strftime("%d/%B/%Y %H:%M:%S UTC", time.gmtime(self.created_utc)))
|
||||
return time.strftime("%d/%B/%Y %H:%M:%S UTC", time.gmtime(self.created_utc))
|
||||
|
||||
@property
|
||||
@lazy
|
||||
|
@ -178,7 +178,7 @@ class Submission(Base):
|
|||
@property
|
||||
@lazy
|
||||
def edited_datetime(self):
|
||||
return str(time.strftime("%d/%B/%Y %H:%M:%S UTC", time.gmtime(self.edited_utc)))
|
||||
return time.strftime("%d/%B/%Y %H:%M:%S UTC", time.gmtime(self.edited_utc))
|
||||
|
||||
|
||||
@property
|
||||
|
@ -331,6 +331,7 @@ class Submission(Base):
|
|||
return data
|
||||
|
||||
def award_count(self, kind):
|
||||
if not FEATURES['AWARDS']: return 0
|
||||
return len([x for x in self.awards if x.kind == kind])
|
||||
|
||||
@lazy
|
||||
|
@ -340,8 +341,8 @@ class Submission(Base):
|
|||
url = self.url.replace("old.reddit.com", v.reddit)
|
||||
|
||||
if '/comments/' in url and "sort=" not in url:
|
||||
if "?" in url: url += "&context=9"
|
||||
else: url += "?context=8"
|
||||
if "?" in url: url += f"&context={RENDER_DEPTH_LIMIT}"
|
||||
else: url += f"?context={RENDER_DEPTH_LIMIT - 1}"
|
||||
if v.controversial: url += "&sort=controversial"
|
||||
return url
|
||||
elif self.url:
|
||||
|
@ -376,22 +377,13 @@ class Submission(Base):
|
|||
|
||||
def plainbody(self, v):
|
||||
if self.club and not (v and (v.paid_dues or v.id == self.author_id)): return f"<p>{CC} ONLY</p>"
|
||||
|
||||
body = self.body
|
||||
|
||||
if not body: return ""
|
||||
|
||||
if v:
|
||||
body = body.replace("old.reddit.com", v.reddit)
|
||||
|
||||
if v.nitter and '/i/' not in body and '/retweets' not in body: body = body.replace("www.twitter.com", "nitter.net").replace("twitter.com", "nitter.net")
|
||||
|
||||
return body
|
||||
|
||||
def print(self):
|
||||
print(f'post: {self.id}, author: {self.author_id}', flush=True)
|
||||
return ''
|
||||
|
||||
@lazy
|
||||
def realtitle(self, v):
|
||||
if self.title_html:
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
from sqlalchemy.orm import deferred, aliased
|
||||
from secrets import token_hex
|
||||
import pyotp
|
||||
from files.helpers.images import *
|
||||
from files.helpers.media import *
|
||||
from files.helpers.const import *
|
||||
from .alts import Alt
|
||||
from .saves import *
|
||||
|
@ -44,7 +44,6 @@ class User(Base):
|
|||
theme = Column(String, default=defaulttheme, nullable=False)
|
||||
themecolor = Column(String, default=DEFAULT_COLOR, nullable=False)
|
||||
cardview = Column(Boolean, default=cardview, nullable=False)
|
||||
song = Column(String)
|
||||
highres = Column(String)
|
||||
profileurl = Column(String)
|
||||
bannerurl = Column(String)
|
||||
|
@ -196,13 +195,10 @@ class User(Base):
|
|||
@property
|
||||
@lazy
|
||||
def user_awards(self):
|
||||
|
||||
return_value = list(AWARDS2.values())
|
||||
|
||||
if not FEATURES['AWARDS']: return []
|
||||
return_value = list(AWARDS_ENABLED.values())
|
||||
user_awards = g.db.query(AwardRelationship).filter_by(user_id=self.id)
|
||||
|
||||
for val in return_value: val['owned'] = user_awards.filter_by(kind=val['kind'], submission_id=None, comment_id=None).count()
|
||||
|
||||
return return_value
|
||||
|
||||
@property
|
||||
|
@ -352,7 +348,7 @@ class User(Base):
|
|||
@property
|
||||
@lazy
|
||||
def received_awards(self):
|
||||
|
||||
if not FEATURES['AWARDS']: return []
|
||||
awards = {}
|
||||
|
||||
posts_idlist = [x[0] for x in g.db.query(Submission.id).filter_by(author_id=self.id).all()]
|
||||
|
@ -563,7 +559,7 @@ class User(Base):
|
|||
@property
|
||||
@lazy
|
||||
def created_datetime(self):
|
||||
return str(time.strftime("%d/%B/%Y %H:%M:%S UTC", time.gmtime(self.created_utc)))
|
||||
return time.strftime("%d/%B/%Y %H:%M:%S UTC", time.gmtime(self.created_utc))
|
||||
|
||||
@lazy
|
||||
def subscribed_idlist(self, page=1):
|
||||
|
@ -620,3 +616,9 @@ class User(Base):
|
|||
l = [i.strip() for i in self.custom_filter_list.split('\n')] if self.custom_filter_list else []
|
||||
l = [i for i in l if i]
|
||||
return l
|
||||
|
||||
# Permissions
|
||||
|
||||
@property
|
||||
def can_see_shadowbanned(self):
|
||||
return self.admin_level >= PERMS['USER_SHADOWBAN'] or self.shadowbanned
|
||||
|
|
|
@ -1,38 +1,41 @@
|
|||
import hashlib
|
||||
import math
|
||||
from typing import Optional
|
||||
import sqlalchemy
|
||||
from werkzeug.security import generate_password_hash
|
||||
from files.__main__ import app
|
||||
from files.classes import User, Submission, Comment, Vote, CommentVote
|
||||
from flask_sqlalchemy import SQLAlchemy
|
||||
from sqlalchemy.orm import scoped_session
|
||||
|
||||
db = SQLAlchemy(app)
|
||||
from werkzeug.security import generate_password_hash
|
||||
|
||||
from files.__main__ import app, db_session
|
||||
from files.classes import User, Submission, Comment, Vote, CommentVote
|
||||
from files.helpers.comments import bulk_recompute_descendant_counts
|
||||
|
||||
@app.cli.command('seed_db')
|
||||
def seed_db():
|
||||
seed_db_worker()
|
||||
|
||||
def seed_db_worker(num_users = 900, num_posts = 40, num_toplevel_comments = 1000, num_reply_comments = 400):
|
||||
"""
|
||||
Seed the database with some example data.
|
||||
"""
|
||||
NUM_USERS = 900;
|
||||
NUM_POSTS = 40;
|
||||
NUM_TOPLEVEL_COMMENTS = 1000
|
||||
NUM_REPLY_COMMENTS = 4000
|
||||
POST_UPVOTE_PROB = 0.020
|
||||
POST_DOWNVOTE_PROB = 0.005
|
||||
COMMENT_UPVOTE_PROB = 0.0008
|
||||
COMMENT_DOWNVOTE_PROB = 0.0003
|
||||
|
||||
db: scoped_session = db_session()
|
||||
|
||||
def detrand():
|
||||
detrand.randstate = bytes(hashlib.sha256(detrand.randstate).hexdigest(), 'utf-8')
|
||||
return int(detrand.randstate, 16) / 2**256
|
||||
|
||||
detrand.randstate = bytes(hashlib.sha256(b'init').hexdigest(), 'utf-8')
|
||||
|
||||
users = db.session.query(User).where(User.id >= 10).all()
|
||||
posts = db.session.query(Submission).all()
|
||||
comments = db.session.query(Comment).all()
|
||||
users = db.query(User).where(User.id >= 10).all()
|
||||
posts = db.query(Submission).all()
|
||||
comments = db.query(Comment).all()
|
||||
|
||||
admin = db.session.query(User).filter(User.id == 9).first()
|
||||
admin = db.query(User).filter(User.id == 9).first()
|
||||
if admin is None:
|
||||
admin = User(**{
|
||||
"username": "admin",
|
||||
|
@ -43,7 +46,7 @@ def seed_db():
|
|||
"ban_evade":0,
|
||||
"profileurl":"/e/feather.webp"
|
||||
})
|
||||
db.session.add(admin)
|
||||
db.add(admin)
|
||||
|
||||
class UserWithFastPasswordHash(User):
|
||||
def hash_password(self, password):
|
||||
|
@ -55,10 +58,10 @@ def seed_db():
|
|||
salt_length=8
|
||||
)
|
||||
|
||||
print(f"Creating {NUM_USERS} users")
|
||||
users_by_id = {user_id: None for user_id in range(10, 10 + NUM_USERS)}
|
||||
print(f"Creating {num_users} users")
|
||||
users_by_id: dict[int, Optional[User]] = {user_id: None for user_id in range(10, 10 + num_users)}
|
||||
for user_id, user in users_by_id.items():
|
||||
user = db.session.query(User).filter(User.id == user_id).first()
|
||||
user = db.query(User).filter(User.id == user_id).first()
|
||||
if user is None:
|
||||
user = UserWithFastPasswordHash(**{
|
||||
"username": f"user{user_id:03d}",
|
||||
|
@ -69,22 +72,22 @@ def seed_db():
|
|||
"ban_evade":0,
|
||||
"profileurl":"/e/feather.webp"
|
||||
})
|
||||
db.session.add(user)
|
||||
db.add(user)
|
||||
users_by_id[user_id] = user
|
||||
|
||||
db.session.commit()
|
||||
db.session.flush()
|
||||
db.commit()
|
||||
db.flush()
|
||||
|
||||
users = list(users_by_id.values())
|
||||
|
||||
db.session.commit()
|
||||
db.session.flush()
|
||||
db.commit()
|
||||
db.flush()
|
||||
|
||||
posts = []
|
||||
|
||||
print(f"Creating {NUM_POSTS} posts")
|
||||
print(f"Creating {num_posts} posts")
|
||||
# 40 top-level posts
|
||||
for i in range(NUM_POSTS):
|
||||
for i in range(num_posts):
|
||||
user = users[int(len(users) * detrand())]
|
||||
post = Submission(
|
||||
private=False,
|
||||
|
@ -102,16 +105,15 @@ def seed_db():
|
|||
ghost=False,
|
||||
filter_state='normal'
|
||||
)
|
||||
db.session.add(post)
|
||||
db.add(post)
|
||||
posts.append(post)
|
||||
|
||||
db.session.commit()
|
||||
db.session.flush()
|
||||
db.commit()
|
||||
|
||||
print(f"Creating {NUM_TOPLEVEL_COMMENTS} top-level comments")
|
||||
print(f"Creating {num_toplevel_comments} top-level comments")
|
||||
comments = []
|
||||
# 2k top-level comments, distributed by power-law
|
||||
for i in range(NUM_TOPLEVEL_COMMENTS):
|
||||
for i in range(num_toplevel_comments):
|
||||
user = users[int(len(users) * detrand())]
|
||||
parent = posts[int(-math.log(detrand()) / math.log(1.4))]
|
||||
comment = Comment(
|
||||
|
@ -126,22 +128,22 @@ def seed_db():
|
|||
body=f'toplevel {i}',
|
||||
ghost=False
|
||||
)
|
||||
db.session.add(comment)
|
||||
db.add(comment)
|
||||
comments.append(comment)
|
||||
|
||||
db.session.flush()
|
||||
db.flush()
|
||||
for c in comments:
|
||||
c.top_comment_id = c.id
|
||||
db.session.add(c)
|
||||
db.add(c)
|
||||
|
||||
db.session.commit()
|
||||
db.commit()
|
||||
|
||||
print(f"Creating {NUM_REPLY_COMMENTS} reply comments")
|
||||
for i in range(NUM_REPLY_COMMENTS):
|
||||
print(f"Creating {num_reply_comments} reply comments")
|
||||
for i in range(num_reply_comments):
|
||||
user = users[int(len(users) * detrand())]
|
||||
parent = comments[int(len(comments) * detrand())]
|
||||
if parent.id is None:
|
||||
db.session.commit()
|
||||
db.commit()
|
||||
comment = Comment(
|
||||
author_id=user.id,
|
||||
parent_submission=str(parent.post.id),
|
||||
|
@ -155,18 +157,18 @@ def seed_db():
|
|||
body=f'reply {i}',
|
||||
ghost=False
|
||||
)
|
||||
db.session.add(comment)
|
||||
db.add(comment)
|
||||
comments.append(comment)
|
||||
|
||||
db.session.commit()
|
||||
db.commit()
|
||||
|
||||
print("Updating comment counts for all posts")
|
||||
for post in posts:
|
||||
post.comment_count = len(post.comments)
|
||||
db.session.merge(post)
|
||||
db.merge(post)
|
||||
|
||||
print("Adding upvotes and downvotes to posts")
|
||||
postvotes = db.session.query(Vote).all()
|
||||
postvotes = db.query(Vote).all()
|
||||
postvotes_pk_set = set((v.submission_id, v.user_id) for v in postvotes)
|
||||
|
||||
for user in users:
|
||||
|
@ -189,10 +191,10 @@ def seed_db():
|
|||
app_id=None,
|
||||
real=True
|
||||
)
|
||||
db.session.add(vote)
|
||||
db.add(vote)
|
||||
|
||||
print("Adding upvotes and downvotes to comments")
|
||||
commentvotes = db.session.query(CommentVote).all()
|
||||
commentvotes = db.query(CommentVote).all()
|
||||
commentvotes_pk_set = set((v.comment_id, v.user_id) for v in commentvotes)
|
||||
|
||||
for user in users:
|
||||
|
@ -215,34 +217,33 @@ def seed_db():
|
|||
app_id=None,
|
||||
real=True
|
||||
)
|
||||
db.session.add(vote)
|
||||
db.add(vote)
|
||||
|
||||
db.session.commit()
|
||||
db.session.flush()
|
||||
db.commit()
|
||||
|
||||
post_upvote_counts = dict(
|
||||
db.session
|
||||
db
|
||||
.query(Vote.submission_id, sqlalchemy.func.count(1))
|
||||
.filter(Vote.vote_type == +1)
|
||||
.group_by(Vote.submission_id)
|
||||
.all()
|
||||
)
|
||||
post_downvote_counts = dict(
|
||||
db.session
|
||||
db
|
||||
.query(Vote.submission_id, sqlalchemy.func.count(1))
|
||||
.filter(Vote.vote_type == -1)
|
||||
.group_by(Vote.submission_id)
|
||||
.all()
|
||||
)
|
||||
comment_upvote_counts = dict(
|
||||
db.session
|
||||
db
|
||||
.query(CommentVote.comment_id, sqlalchemy.func.count(1))
|
||||
.filter(CommentVote.vote_type == +1)
|
||||
.group_by(CommentVote.comment_id)
|
||||
.all()
|
||||
)
|
||||
comment_downvote_counts = dict(
|
||||
db.session
|
||||
db
|
||||
.query(CommentVote.comment_id, sqlalchemy.func.count(1))
|
||||
.filter(CommentVote.vote_type == -1)
|
||||
.group_by(CommentVote.comment_id)
|
||||
|
@ -253,13 +254,15 @@ def seed_db():
|
|||
post.upvotes = post_upvote_counts.get(post.id, 0)
|
||||
post.downvotes = post_downvote_counts.get(post.id, 0)
|
||||
post.realupvotes = post.upvotes - post.downvotes
|
||||
db.session.add(post)
|
||||
db.add(post)
|
||||
|
||||
for comment in comments:
|
||||
comment.upvotes = comment_upvote_counts.get(comment.id, 0)
|
||||
comment.downvotes = comment_downvote_counts.get(comment.id, 0)
|
||||
comment.realupvotes = comment.upvotes - comment.downvotes
|
||||
db.session.add(comment)
|
||||
db.add(comment)
|
||||
|
||||
db.session.commit()
|
||||
db.session.flush()
|
||||
print("Computing comment descendant_count")
|
||||
bulk_recompute_descendant_counts(db=db)
|
||||
|
||||
db.commit()
|
||||
|
|
14
files/helpers/captcha.py
Normal file
14
files/helpers/captcha.py
Normal file
|
@ -0,0 +1,14 @@
|
|||
from typing import Final
|
||||
import requests
|
||||
|
||||
HCAPTCHA_URL: Final[str] = "https://hcaptcha.com/siteverify"
|
||||
|
||||
def validate_captcha(secret:str, sitekey: str, token: str):
|
||||
if not sitekey: return True
|
||||
if not token: return False
|
||||
data = {"secret": secret,
|
||||
"response": token,
|
||||
"sitekey": sitekey
|
||||
}
|
||||
req = requests.post(HCAPTCHA_URL, data=data, timeout=5)
|
||||
return bool(req.json()["success"])
|
|
@ -1,15 +1,15 @@
|
|||
from pusher_push_notifications import PushNotifications
|
||||
from files.classes import Comment, Notification, Subscription
|
||||
from files.classes import Comment, Notification, Subscription, User
|
||||
from files.helpers.alerts import NOTIFY_USERS
|
||||
from files.helpers.const import PUSHER_ID, PUSHER_KEY, SITE_ID, SITE_FULL
|
||||
from files.helpers.assetcache import assetcache_path
|
||||
from flask import g
|
||||
from sqlalchemy import select, update
|
||||
from sqlalchemy.sql.expression import func, text, alias
|
||||
from sqlalchemy.orm import aliased
|
||||
from sqlalchemy.orm import Query, aliased
|
||||
from sys import stdout
|
||||
import gevent
|
||||
import typing
|
||||
from typing import Optional
|
||||
|
||||
if PUSHER_ID != 'blahblahblah':
|
||||
beams_client = PushNotifications(instance_id=PUSHER_ID, secret_key=PUSHER_KEY)
|
||||
|
@ -141,13 +141,13 @@ def bulk_recompute_descendant_counts(predicate = None, db=None):
|
|||
True
|
||||
)
|
||||
.group_by(parent_comments.corresponding_column(Comment.id))
|
||||
.with_only_columns([
|
||||
.with_only_columns(
|
||||
parent_comments.corresponding_column(Comment.id),
|
||||
func.coalesce(
|
||||
func.sum(child_comments.corresponding_column(Comment.descendant_count) + text(str(1))),
|
||||
text(str(0))
|
||||
).label('descendant_count')
|
||||
])
|
||||
)
|
||||
.subquery(name='descendant_counts')
|
||||
),
|
||||
adapt_on_names=True
|
||||
|
@ -217,3 +217,16 @@ def comment_on_unpublish(comment:Comment):
|
|||
reflect the comments users will actually see.
|
||||
"""
|
||||
update_stateful_counters(comment, -1)
|
||||
|
||||
|
||||
def comment_filter_moderated(q: Query, v: Optional[User]) -> Query:
|
||||
if not (v and v.shadowbanned) and not (v and v.admin_level > 2):
|
||||
q = q.join(User, User.id == Comment.author_id) \
|
||||
.filter(User.shadowbanned == None)
|
||||
if not v or v.admin_level < 2:
|
||||
q = q.filter(
|
||||
((Comment.filter_state != 'filtered')
|
||||
& (Comment.filter_state != 'removed'))
|
||||
| (Comment.author_id == ((v and v.id) or 0))
|
||||
)
|
||||
return q
|
||||
|
|
|
@ -1,10 +1,12 @@
|
|||
from os import environ, listdir
|
||||
import re
|
||||
from copy import deepcopy
|
||||
from json import loads
|
||||
from os import environ
|
||||
from typing import Final
|
||||
|
||||
from flask import request
|
||||
|
||||
from files.__main__ import db_session
|
||||
from files.classes.marsey import Marsey
|
||||
from flask import request
|
||||
|
||||
SITE = environ.get("DOMAIN", '').strip()
|
||||
SITE_ID = environ.get("SITE_ID", '').strip()
|
||||
|
@ -17,6 +19,7 @@ CC_TITLE = CC.title()
|
|||
|
||||
NOTIFICATIONS_ID = 1
|
||||
AUTOJANNY_ID = 2
|
||||
MODMAIL_ID = 2
|
||||
SNAPPY_ID = 3
|
||||
LONGPOSTBOT_ID = 4
|
||||
ZOZBOT_ID = 5
|
||||
|
@ -31,10 +34,25 @@ BUG_THREAD = 0
|
|||
WELCOME_MSG = f"Welcome to {SITE_TITLE}! Please read [the rules](/rules) first. Then [read some of our current conversations](/) and feel free to comment or post!\n\nWe encourage people to comment even if they aren't sure they fit in; as long as your comment follows [community rules](/rules), we are happy to have posters from all backgrounds, education levels, and specialties."
|
||||
ROLES={}
|
||||
|
||||
LEADERBOARD_LIMIT: Final[int] = 25
|
||||
|
||||
THEMES = {"TheMotte", "dramblr", "reddit", "transparent", "win98", "dark",
|
||||
"light", "coffee", "tron", "4chan", "midnight"}
|
||||
SORTS_COMMON = {
|
||||
"top": 'fa-arrow-alt-circle-up',
|
||||
"bottom": 'fa-arrow-alt-circle-down',
|
||||
"new": 'fa-sparkles',
|
||||
"old": 'fa-book',
|
||||
"controversial": 'fa-bullhorn',
|
||||
"comments": 'fa-comments'
|
||||
}
|
||||
SORTS_POSTS = {
|
||||
"hot": "fa-fire",
|
||||
"bump": "fa-arrow-up"
|
||||
}
|
||||
SORTS_POSTS.update(SORTS_COMMON)
|
||||
SORTS_COMMENTS = SORTS_COMMON
|
||||
|
||||
IMGUR_KEY = environ.get("IMGUR_KEY").strip()
|
||||
PUSHER_ID = environ.get("PUSHER_ID", "").strip()
|
||||
PUSHER_KEY = environ.get("PUSHER_KEY", "").strip()
|
||||
DEFAULT_COLOR = environ.get("DEFAULT_COLOR", "fff").strip()
|
||||
|
@ -54,6 +72,10 @@ ERROR_MESSAGES = {
|
|||
}
|
||||
|
||||
LOGGEDIN_ACTIVE_TIME = 15 * 60
|
||||
RENDER_DEPTH_LIMIT = 9
|
||||
'''
|
||||
The maximum depth at which a comment tree is rendered
|
||||
'''
|
||||
|
||||
WERKZEUG_ERROR_DESCRIPTIONS = {
|
||||
400: "The browser (or proxy) sent a request that this server could not understand.",
|
||||
|
@ -74,125 +96,29 @@ VIDEO_FORMATS = ['mp4','webm','mov','avi','mkv','flv','m4v','3gp']
|
|||
AUDIO_FORMATS = ['mp3','wav','ogg','aac','m4a','flac']
|
||||
NO_TITLE_EXTENSIONS = IMAGE_FORMATS + VIDEO_FORMATS + AUDIO_FORMATS
|
||||
|
||||
FEATURES = {
|
||||
"AWARDS": False,
|
||||
}
|
||||
|
||||
PERMS = {
|
||||
"DEBUG_LOGIN_TO_OTHERS": 3,
|
||||
"USER_SHADOWBAN": 2,
|
||||
}
|
||||
|
||||
AWARDS = {
|
||||
"lootbox": {
|
||||
"kind": "lootbox",
|
||||
"title": "Lootstocking",
|
||||
"description": "???",
|
||||
"icon": "fas fa-stocking",
|
||||
"color": "text-danger",
|
||||
"price": 1000
|
||||
},
|
||||
"shit": {
|
||||
"kind": "shit",
|
||||
"title": "Shit",
|
||||
"description": "Makes flies swarm the post.",
|
||||
"icon": "fas fa-poop",
|
||||
"color": "text-black-50",
|
||||
"price": 300
|
||||
},
|
||||
"fireflies": {
|
||||
"kind": "fireflies",
|
||||
"title": "Fireflies",
|
||||
"description": "Makes fireflies swarm the post.",
|
||||
"icon": "fas fa-sparkles",
|
||||
"color": "text-warning",
|
||||
"price": 300
|
||||
},
|
||||
"train": {
|
||||
"kind": "train",
|
||||
"title": "Train",
|
||||
"description": "Summons a train on the post.",
|
||||
"icon": "fas fa-train",
|
||||
"color": "text-pink",
|
||||
"price": 300
|
||||
},
|
||||
"scooter": {
|
||||
"kind": "scooter",
|
||||
"title": "Scooter",
|
||||
"description": "Summons a scooter on the post.",
|
||||
"icon": "fas fa-flag-usa",
|
||||
"color": "text-muted",
|
||||
"price": 300
|
||||
},
|
||||
"wholesome": {
|
||||
"kind": "wholesome",
|
||||
"title": "Wholesome",
|
||||
"description": "Summons a wholesome marsey on the post.",
|
||||
"icon": "fas fa-smile-beam",
|
||||
"color": "text-yellow",
|
||||
"price": 300
|
||||
},
|
||||
"glowie": {
|
||||
"kind": "glowie",
|
||||
"title": "Glowie",
|
||||
"description": "Indicates that the recipient can be seen when driving. Just run them over.",
|
||||
"icon": "fas fa-user-secret",
|
||||
"color": "text-green",
|
||||
"price": 300
|
||||
},
|
||||
"pin": {
|
||||
"kind": "pin",
|
||||
"title": "1-Hour Pin",
|
||||
"description": "Pins the post/comment.",
|
||||
"icon": "fas fa-thumbtack fa-rotate--45",
|
||||
"color": "text-warning",
|
||||
"price": 1000
|
||||
},
|
||||
"unpin": {
|
||||
"kind": "unpin",
|
||||
"title": "1-Hour Unpin",
|
||||
"description": "Removes 1 hour from the pin duration of the post/comment.",
|
||||
"icon": "fas fa-thumbtack fa-rotate--45",
|
||||
"color": "text-black",
|
||||
"price": 1000
|
||||
},
|
||||
"ban": {
|
||||
"kind": "ban",
|
||||
"title": "1-Day Ban",
|
||||
"description": "Bans the recipient for a day.",
|
||||
"icon": "fas fa-gavel",
|
||||
"color": "text-danger",
|
||||
"price": 3000
|
||||
},
|
||||
"unban": {
|
||||
"kind": "unban",
|
||||
"title": "1-Day Unban",
|
||||
"description": "Removes 1 day from the ban duration of the recipient.",
|
||||
"icon": "fas fa-gavel",
|
||||
"color": "text-success",
|
||||
"price": 3500
|
||||
},
|
||||
"benefactor": {
|
||||
"kind": "benefactor",
|
||||
"title": "Benefactor",
|
||||
"description": "Grants one month of paypig status and 2500 marseybux to the recipient. Cannot be used on yourself.",
|
||||
"icon": "fas fa-gift",
|
||||
"color": "text-blue",
|
||||
"price": 4000
|
||||
},
|
||||
"grass": {
|
||||
"kind": "grass",
|
||||
"title": "Grass",
|
||||
"description": "Doesn't do anything",
|
||||
"icon": "fas fa-seedling",
|
||||
"color": "text-success",
|
||||
"price": 10000
|
||||
},
|
||||
}
|
||||
AWARDS = {}
|
||||
|
||||
AWARDS2 = deepcopy(AWARDS)
|
||||
for k, val in AWARDS.items():
|
||||
if val['description'] == '???': AWARDS2.pop(k)
|
||||
if FEATURES['AWARDS']:
|
||||
AWARDS_ENABLED = deepcopy(AWARDS)
|
||||
for k, val in AWARDS.items():
|
||||
if val['description'] == '???': AWARDS_ENABLED.pop(k)
|
||||
|
||||
AWARDS_JL2_PRINTABLE = {}
|
||||
for k, val in AWARDS_ENABLED.items():
|
||||
if val['price'] == 300: AWARDS_JL2_PRINTABLE[k] = val
|
||||
else:
|
||||
AWARDS_ENABLED = {}
|
||||
AWARDS_JL2_PRINTABLE = {}
|
||||
|
||||
AWARDS3 = {}
|
||||
for k, val in AWARDS2.items():
|
||||
if val['price'] == 300: AWARDS3[k] = val
|
||||
|
||||
NOTIFIED_USERS = {
|
||||
# format: 'substring' ↦ User ID to notify
|
||||
|
|
|
@ -1,9 +1,17 @@
|
|||
import time
|
||||
from collections.abc import Iterable
|
||||
from typing import Any, TYPE_CHECKING
|
||||
|
||||
from sqlalchemy.sql import func
|
||||
from sqlalchemy.orm import Query
|
||||
|
||||
from files.helpers.const import *
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from files.classes.comment import Comment
|
||||
else:
|
||||
Comment = Any
|
||||
|
||||
|
||||
def apply_time_filter(objects, t, cls):
|
||||
now = int(time.time())
|
||||
|
@ -22,66 +30,75 @@ def apply_time_filter(objects, t, cls):
|
|||
return objects.filter(cls.created_utc >= cutoff)
|
||||
|
||||
|
||||
def sort_objects(objects, sort, cls):
|
||||
def sort_objects(objects: Query, sort: str, cls):
|
||||
if sort == 'hot':
|
||||
ti = int(time.time()) + 3600
|
||||
return objects.order_by(
|
||||
ordered = objects.order_by(
|
||||
-100000
|
||||
* (cls.upvotes + 1)
|
||||
/ (func.power((ti - cls.created_utc) / 1000, 1.23)),
|
||||
cls.created_utc.desc())
|
||||
/ (func.power((ti - cls.created_utc) / 1000, 1.23)))
|
||||
elif sort == 'bump' and cls.__name__ == 'Submission':
|
||||
return objects.filter(cls.comment_count > 1).order_by(
|
||||
cls.bump_utc.desc(), cls.created_utc.desc())
|
||||
elif sort == 'comments' and cls.__name__ == 'Submission':
|
||||
return objects.order_by(
|
||||
cls.comment_count.desc(), cls.created_utc.desc())
|
||||
ordered = objects.filter(cls.comment_count > 1).order_by(cls.bump_utc.desc())
|
||||
elif sort == 'comments':
|
||||
if cls.__name__ == 'Submission': # we're checking the stringified name due to a gnarly import cycle
|
||||
ordered = objects.order_by(cls.comment_count.desc())
|
||||
elif cls.__name__ == 'Comment':
|
||||
ordered = objects.order_by(cls.descendant_count.desc())
|
||||
else:
|
||||
ordered = objects
|
||||
elif sort == 'controversial':
|
||||
return objects.order_by(
|
||||
ordered = objects.order_by(
|
||||
(cls.upvotes + 1) / (cls.downvotes + 1)
|
||||
+ (cls.downvotes + 1) / (cls.upvotes + 1),
|
||||
cls.downvotes.desc(), cls.created_utc.desc())
|
||||
cls.downvotes.desc())
|
||||
elif sort == 'top':
|
||||
return objects.order_by(
|
||||
cls.downvotes - cls.upvotes, cls.created_utc.desc())
|
||||
ordered = objects.order_by(cls.downvotes - cls.upvotes)
|
||||
elif sort == 'bottom':
|
||||
return objects.order_by(
|
||||
cls.upvotes - cls.downvotes, cls.created_utc.desc())
|
||||
ordered = objects.order_by(cls.upvotes - cls.downvotes)
|
||||
elif sort == 'old':
|
||||
return objects.order_by(cls.created_utc)
|
||||
else: # default, or sort == 'new'
|
||||
return objects.order_by(cls.created_utc.desc())
|
||||
ordered = objects
|
||||
ordered = ordered.order_by(cls.created_utc.desc())
|
||||
return ordered
|
||||
|
||||
|
||||
# Presently designed around files.helpers.get.get_comment_trees_eager
|
||||
# Behavior should parallel that of sort_objects above. TODO: Unify someday?
|
||||
def sort_comment_results(comments, sort):
|
||||
DESC = (2 << 30) - 1 # descending sorts, Y2038 problem, change before then
|
||||
def sort_comment_results(comments: Iterable[Comment], sort:str, *, pins:bool=False):
|
||||
"""
|
||||
Sorts comments results from `files.helpers.get.get_comments_trees_eager`
|
||||
:param comments: Comments to sort
|
||||
:param sort: The sort to use
|
||||
:param pins: Whether to sort pinned comments. Defaults to `True`
|
||||
"""
|
||||
if sort == 'hot':
|
||||
ti = int(time.time()) + 3600
|
||||
key_func = lambda c: (
|
||||
-100000
|
||||
* (c.upvotes + 1)
|
||||
/ (pow(((ti - c.created_utc) / 1000), 1.23)),
|
||||
DESC - c.created_utc
|
||||
-c.created_utc
|
||||
)
|
||||
elif sort == 'comments':
|
||||
key_func = lambda c: -c.descendant_count
|
||||
elif sort == 'controversial':
|
||||
key_func = lambda c: (
|
||||
(c.upvotes + 1) / (c.downvotes + 1)
|
||||
+ (c.downvotes + 1) / (c.upvotes + 1),
|
||||
DESC - c.downvotes,
|
||||
DESC - c.created_utc
|
||||
-c.downvotes,
|
||||
-c.created_utc
|
||||
)
|
||||
elif sort == 'top':
|
||||
key_func = lambda c: (c.downvotes - c.upvotes, DESC - c.created_utc)
|
||||
key_func = lambda c: (c.downvotes - c.upvotes, -c.created_utc)
|
||||
elif sort == 'bottom':
|
||||
key_func = lambda c: (c.upvotes - c.downvotes, DESC - c.created_utc)
|
||||
key_func = lambda c: (c.upvotes - c.downvotes, -c.created_utc)
|
||||
elif sort == 'old':
|
||||
key_func = lambda c: c.created_utc
|
||||
else: # default, or sort == 'new'
|
||||
key_func = lambda c: DESC - c.created_utc
|
||||
key_func = lambda c: -c.created_utc
|
||||
|
||||
key_func_pinned = lambda c: (
|
||||
(c.is_pinned is None, c.is_pinned == '', c.is_pinned), # sort None last
|
||||
key_func(c))
|
||||
return sorted(comments, key=key_func_pinned)
|
||||
return sorted(comments, key=key_func_pinned if pins else key_func)
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
from collections import defaultdict
|
||||
from typing import Iterable, List, Optional, Type, Union
|
||||
from typing import Callable, Iterable, List, Optional, Type, Union
|
||||
|
||||
from flask import g
|
||||
from sqlalchemy import and_, or_, func
|
||||
from sqlalchemy.orm import selectinload
|
||||
from sqlalchemy.orm import Query, scoped_session, selectinload
|
||||
|
||||
from files.classes import *
|
||||
from files.helpers.const import AUTOJANNY_ID
|
||||
|
@ -95,6 +95,24 @@ def get_account(
|
|||
|
||||
return user
|
||||
|
||||
def get_accounts_dict(ids:Union[Iterable[str], Iterable[int]],
|
||||
v:Optional[User]=None, graceful=False,
|
||||
include_shadowbanned=True,
|
||||
db:Optional[scoped_session]=None) -> Optional[dict[int, User]]:
|
||||
if not db: db = g.db
|
||||
if not ids: return {}
|
||||
try:
|
||||
ids = set([int(id) for id in ids])
|
||||
except:
|
||||
if graceful: return None
|
||||
abort(404)
|
||||
|
||||
users = db.query(User).filter(User.id.in_(ids))
|
||||
if not (include_shadowbanned or (v and v.can_see_shadowbanned)):
|
||||
users = users.filter(User.shadowbanned == None)
|
||||
users = users.all()
|
||||
if len(users) != len(ids) and not graceful: abort(404)
|
||||
return {u.id:u for u in users}
|
||||
|
||||
def get_post(
|
||||
i:Union[str,int],
|
||||
|
@ -277,9 +295,9 @@ def get_comments(
|
|||
# TODO: There is probably some way to unify this with get_comments. However, in
|
||||
# the interim, it's a hot path and benefits from having tailored code.
|
||||
def get_comment_trees_eager(
|
||||
top_comment_ids:Iterable[int],
|
||||
sort:str="old",
|
||||
v:Optional[User]=None) -> List[Comment]:
|
||||
query_filter_callable: Callable[[Query], Query],
|
||||
sort: str="old",
|
||||
v: Optional[User]=None) -> tuple[list[Comment], defaultdict[Comment, list[Comment]]]:
|
||||
|
||||
if v:
|
||||
votes = g.db.query(CommentVote).filter_by(user_id=v.id).subquery()
|
||||
|
@ -305,7 +323,7 @@ def get_comment_trees_eager(
|
|||
else:
|
||||
query = g.db.query(Comment)
|
||||
|
||||
query = query.filter(Comment.top_comment_id.in_(top_comment_ids))
|
||||
query = query_filter_callable(query)
|
||||
query = query.options(
|
||||
selectinload(Comment.author).options(
|
||||
selectinload(User.badges),
|
||||
|
@ -335,13 +353,12 @@ def get_comment_trees_eager(
|
|||
comments_map_parent[c.parent_comment_id].append(c)
|
||||
|
||||
for parent_id in comments_map_parent:
|
||||
if parent_id is None: continue
|
||||
|
||||
comments_map_parent[parent_id] = sort_comment_results(
|
||||
comments_map_parent[parent_id], sort)
|
||||
comments_map[parent_id].replies2 = comments_map_parent[parent_id]
|
||||
comments_map_parent[parent_id], sort, pins=True)
|
||||
if parent_id in comments_map:
|
||||
comments_map[parent_id].replies2 = comments_map_parent[parent_id]
|
||||
|
||||
return [comments_map[tcid] for tcid in top_comment_ids]
|
||||
return comments, comments_map_parent
|
||||
|
||||
|
||||
# TODO: This function was concisely inlined into posts.py in upstream.
|
||||
|
|
|
@ -75,6 +75,7 @@ def inject_constants():
|
|||
"SITE_FULL":SITE_FULL,
|
||||
"AUTOJANNY_ID":AUTOJANNY_ID,
|
||||
"NOTIFICATIONS_ID":NOTIFICATIONS_ID,
|
||||
"MODMAIL_ID":MODMAIL_ID,
|
||||
"PUSHER_ID":PUSHER_ID,
|
||||
"CC":CC,
|
||||
"CC_TITLE":CC_TITLE,
|
||||
|
@ -84,6 +85,10 @@ def inject_constants():
|
|||
"COLORS":COLORS,
|
||||
"THEMES":THEMES,
|
||||
"PERMS":PERMS,
|
||||
"FEATURES":FEATURES,
|
||||
"RENDER_DEPTH_LIMIT":RENDER_DEPTH_LIMIT,
|
||||
"SORTS_COMMENTS":SORTS_COMMENTS,
|
||||
"SORTS_POSTS":SORTS_POSTS,
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
from PIL import Image, ImageOps
|
||||
from PIL.ImageSequence import Iterator
|
||||
from webptools import gifwebp
|
||||
import subprocess
|
||||
|
||||
from flask import Request
|
||||
from PIL import Image, ImageOps
|
||||
from webptools import gifwebp
|
||||
|
||||
def process_image(filename=None, resize=0):
|
||||
|
||||
i = Image.open(filename)
|
||||
|
||||
if resize and i.width > resize:
|
|
@ -10,21 +10,43 @@ import re
|
|||
from mistletoe import markdown
|
||||
from json import loads, dump
|
||||
from random import random, choice
|
||||
import signal
|
||||
import gevent
|
||||
import time
|
||||
import requests
|
||||
from files.__main__ import app
|
||||
|
||||
TLDS = ('ac','ad','ae','aero','af','ag','ai','al','am','an','ao','aq','ar','arpa','as','asia','at','au','aw','ax','az','ba','bb','bd','be','bf','bg','bh','bi','biz','bj','bm','bn','bo','br','bs','bt','bv','bw','by','bz','ca','cafe','cat','cc','cd','cf','cg','ch','ci','ck','cl','club','cm','cn','co','com','coop','cr','cu','cv','cx','cy','cz','de','dj','dk','dm','do','dz','ec','edu','ee','eg','er','es','et','eu','fi','fj','fk','fm','fo','fr','ga','gb','gd','ge','gf','gg','gh','gi','gl','gm','gn','gov','gp','gq','gr','gs','gt','gu','gw','gy','hk','hm','hn','hr','ht','hu','id','ie','il','im','in','info','int','io','iq','ir','is','it','je','jm','jo','jobs','jp','ke','kg','kh','ki','km','kn','kp','kr','kw','ky','kz','la','lb','lc','li','lk','lr','ls','lt','lu','lv','ly','ma','mc','md','me','mg','mh','mil','mk','ml','mm','mn','mo','mobi','mp','mq','mr','ms','mt','mu','museum','mv','mw','mx','my','mz','na','name','nc','ne','net','nf','ng','ni','nl','no','np','nr','nu','nz','om','org','pa','pe','pf','pg','ph','pk','pl','pm','pn','post','pr','pro','ps','pt','pw','py','qa','re','ro','rs','ru','rw','sa','sb','sc','sd','se','sg','sh','si','sj','sk','sl','sm','sn','so','social','sr','ss','st','su','sv','sx','sy','sz','tc','td','tel','tf','tg','th','tj','tk','tl','tm','tn','to','tp','tr','travel','tt','tv','tw','tz','ua','ug','uk','us','uy','uz','va','vc','ve','vg','vi','vn','vu','wf','win','ws','xn','xxx','xyz','ye','yt','yu','za','zm','zw', 'moe')
|
||||
TLDS = ('ac','ad','ae','aero','af','ag','ai','al','am','an','ao','aq','ar',
|
||||
'arpa','as','asia','at','au','aw','ax','az','ba','bb','bd','be','bf','bg',
|
||||
'bh','bi','biz','bj','bm','bn','bo','br','bs','bt','bv','bw','by','bz',
|
||||
'ca','cafe','cat','cc','cd','cf','cg','ch','ci','ck','cl','club','cm',
|
||||
'cn','co','com','coop','cr','cu','cv','cx','cy','cz','de','dj','dk','dm',
|
||||
'do','dz','ec','edu','ee','eg','er','es','et','eu','fi','fj','fk','fm',
|
||||
'fo','fr','ga','gb','gd','ge','gf','gg','gh','gi','gl','gm','gn','gov',
|
||||
'gp','gq','gr','gs','gt','gu','gw','gy','hk','hm','hn','hr','ht','hu',
|
||||
'id','ie','il','im','in','info','int','io','iq','ir','is','it','je','jm',
|
||||
'jo','jobs','jp','ke','kg','kh','ki','km','kn','kp','kr','kw','ky','kz',
|
||||
'la','lb','lc','li','lk','lr','ls','lt','lu','lv','ly','ma','mc','md','me',
|
||||
'mg','mh','mil','mk','ml','mm','mn','mo','mobi','mp','mq','mr','ms','mt',
|
||||
'mu','museum','mv','mw','mx','my','mz','na','name','nc','ne','net','nf',
|
||||
'ng','ni','nl','no','np','nr','nu','nz','om','org','pa','pe','pf','pg',
|
||||
'ph','pk','pl','pm','pn','post','pr','pro','ps','pt','pw','py','qa','re',
|
||||
'ro','rs','ru','rw','sa','sb','sc','sd','se','sg','sh','si','sj','sk',
|
||||
'sl','sm','sn','so','social','sr','ss','st','su','sv','sx','sy','sz',
|
||||
'tc','td','tel','tf','tg','th','tj','tk','tl','tm','tn','to','tp','tr',
|
||||
'travel','tt','tv','tw','tz','ua','ug','uk','us','uy','uz','va','vc','ve',
|
||||
'vg','vi','vn','vu','wf','win','ws','xn','xxx','xyz','ye','yt','yu','za',
|
||||
'zm','zw', 'moe')
|
||||
|
||||
allowed_tags = ('b','blockquote','br','code','del','em','h1','h2','h3','h4','h5','h6','hr','i','li','ol','p','pre','strong','sub','sup','table','tbody','th','thead','td','tr','ul','a','span','ruby','rp','rt','spoiler',)
|
||||
allowed_tags = ('b','blockquote','br','code','del','em','h1','h2','h3','h4',
|
||||
'h5','h6','hr','i','li','ol','p','pre','strong','sub','sup','table',
|
||||
'tbody','th','thead','td','tr','ul','a','span','ruby','rp','rt',
|
||||
'spoiler',)
|
||||
|
||||
if app.config['MULTIMEDIA_EMBEDDING_ENABLED']:
|
||||
allowed_tags += ('img', 'lite-youtube', 'video', 'source',)
|
||||
|
||||
|
||||
def allowed_attributes(tag, name, value):
|
||||
|
||||
if name == 'style': return True
|
||||
|
||||
if tag == 'a':
|
||||
|
@ -123,31 +145,39 @@ def render_emoji(html, regexp, edit, marseys_used=set(), b=False):
|
|||
return html
|
||||
|
||||
|
||||
def with_sigalrm_timeout(timeout: int):
|
||||
'Use SIGALRM to raise an exception if the function executes for longer than timeout seconds'
|
||||
|
||||
# while trying to test this using time.sleep I discovered that gunicorn does in fact do some
|
||||
# async so if we timeout on that (or on a db op) then the process is crashed without returning
|
||||
# a proper 500 error. Oh well.
|
||||
def sig_handler(signum, frame):
|
||||
print("Timeout!", flush=True)
|
||||
raise Exception("Timeout")
|
||||
|
||||
def with_gevent_timeout(timeout: int):
|
||||
'''
|
||||
Use gevent to raise an exception if the function executes for longer than timeout seconds
|
||||
Using gevent instead of a signal based approach allows for proper async and avoids some
|
||||
worker crashes
|
||||
'''
|
||||
def inner(func):
|
||||
@functools.wraps(inner)
|
||||
@functools.wraps(func)
|
||||
def wrapped(*args, **kwargs):
|
||||
signal.signal(signal.SIGALRM, sig_handler)
|
||||
signal.alarm(timeout)
|
||||
try:
|
||||
return func(*args, **kwargs)
|
||||
finally:
|
||||
signal.alarm(0)
|
||||
return gevent.with_timeout(timeout, func, *args, **kwargs)
|
||||
return wrapped
|
||||
return inner
|
||||
|
||||
@with_sigalrm_timeout(2)
|
||||
def sanitize(sanitized, alert=False, comment=False, edit=False):
|
||||
REMOVED_CHARACTERS = ['\u200e', '\u200b', '\ufeff']
|
||||
"""
|
||||
Characters which are removed from content
|
||||
"""
|
||||
|
||||
def sanitize_raw(sanitized:Optional[str], allow_newlines:bool, length_limit:Optional[int]) -> str:
|
||||
if not sanitized: return ""
|
||||
for char in REMOVED_CHARACTERS:
|
||||
sanitized = sanitized.replace(char, '')
|
||||
if allow_newlines:
|
||||
sanitized = sanitized.replace("\r\n", "\n")
|
||||
else:
|
||||
sanitized = sanitized.replace("\r","").replace("\n", "")
|
||||
sanitized = sanitized.strip()
|
||||
if length_limit is not None:
|
||||
sanitized = sanitized[:length_limit]
|
||||
return sanitized
|
||||
|
||||
@with_gevent_timeout(2)
|
||||
def sanitize(sanitized, alert=False, comment=False, edit=False):
|
||||
# double newlines, eg. hello\nworld becomes hello\n\nworld, which later becomes <p>hello</p><p>world</p>
|
||||
sanitized = linefeeds_regex.sub(r'\1\n\n\2', sanitized)
|
||||
|
||||
|
@ -186,15 +216,11 @@ def sanitize(sanitized, alert=False, comment=False, edit=False):
|
|||
sanitized = sub_regex.sub(r'\1<a href="/\2">/\2</a>', sanitized)
|
||||
|
||||
matches = [ m for m in mention_regex.finditer(sanitized) if m ]
|
||||
names = set( m.group(2) for m in matches )
|
||||
names = set(m.group(2) for m in matches)
|
||||
users = get_users(names,graceful=True)
|
||||
|
||||
if len(users) > app.config['MENTION_LIMIT']:
|
||||
signal.alarm(0)
|
||||
abort(
|
||||
make_response(
|
||||
jsonify(
|
||||
error=f'Mentioned {len(users)} users but limit is {app.config["MENTION_LIMIT"]}'), 400))
|
||||
abort(400, f'Mentioned {len(users)} users but limit is {app.config["MENTION_LIMIT"]}')
|
||||
|
||||
for u in users:
|
||||
if not u: continue
|
||||
|
@ -281,12 +307,8 @@ def sanitize(sanitized, alert=False, comment=False, edit=False):
|
|||
sanitized = sanitized.replace('&','&')
|
||||
sanitized = utm_regex.sub('', sanitized)
|
||||
sanitized = utm_regex2.sub('', sanitized)
|
||||
|
||||
|
||||
sanitized = sanitized.replace('<html><body>','').replace('</body></html>','')
|
||||
|
||||
|
||||
|
||||
sanitized = bleach.Cleaner(tags=allowed_tags,
|
||||
attributes=allowed_attributes,
|
||||
protocols=['http', 'https'],
|
||||
|
@ -321,17 +343,11 @@ def sanitize(sanitized, alert=False, comment=False, edit=False):
|
|||
domain_list.add(new_domain)
|
||||
|
||||
bans = g.db.query(BannedDomain.domain).filter(BannedDomain.domain.in_(list(domain_list))).all()
|
||||
|
||||
if bans: abort(403, description=f"Remove the banned domains {bans} and try again!")
|
||||
|
||||
return sanitized
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
def allowed_attributes_emojis(tag, name, value):
|
||||
|
||||
if tag == 'img':
|
||||
if name == 'loading' and value == 'lazy': return True
|
||||
if name == 'data-bs-toggle' and value == 'tooltip': return True
|
||||
|
@ -339,9 +355,8 @@ def allowed_attributes_emojis(tag, name, value):
|
|||
return False
|
||||
|
||||
|
||||
@with_sigalrm_timeout(1)
|
||||
@with_gevent_timeout(1)
|
||||
def filter_emojis_only(title, edit=False, graceful=False):
|
||||
|
||||
title = unwanted_bytes_regex.sub('', title)
|
||||
title = whitespace_regex.sub(' ', title)
|
||||
title = html.escape(title, quote=True)
|
||||
|
|
59
files/helpers/services.py
Normal file
59
files/helpers/services.py
Normal file
|
@ -0,0 +1,59 @@
|
|||
import sys
|
||||
|
||||
import gevent
|
||||
from pusher_push_notifications import PushNotifications
|
||||
from sqlalchemy.orm import scoped_session
|
||||
|
||||
from files.classes.leaderboard import (LeaderboardMeta, ReceivedDownvotesLeaderboard,
|
||||
GivenUpvotesLeaderboard)
|
||||
from files.helpers.assetcache import assetcache_path
|
||||
from files.helpers.const import PUSHER_ID, PUSHER_KEY, SITE_FULL, SITE_ID
|
||||
from files.__main__ import app, db_session
|
||||
|
||||
if PUSHER_ID != 'blahblahblah':
|
||||
beams_client = PushNotifications(instance_id=PUSHER_ID, secret_key=PUSHER_KEY)
|
||||
else:
|
||||
beams_client = None
|
||||
|
||||
def pusher_thread2(interests, notifbody, username):
|
||||
if not beams_client: return
|
||||
beams_client.publish_to_interests(
|
||||
interests=[interests],
|
||||
publish_body={
|
||||
'web': {
|
||||
'notification': {
|
||||
'title': f'New message from @{username}',
|
||||
'body': notifbody,
|
||||
'deep_link': f'{SITE_FULL}/notifications?messages=true',
|
||||
'icon': SITE_FULL + assetcache_path(f'images/{SITE_ID}/icon.webp'),
|
||||
}
|
||||
},
|
||||
'fcm': {
|
||||
'notification': {
|
||||
'title': f'New message from @{username}',
|
||||
'body': notifbody,
|
||||
},
|
||||
'data': {
|
||||
'url': '/notifications?messages=true',
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
sys.stdout.flush()
|
||||
|
||||
_lb_received_downvotes_meta = LeaderboardMeta("Downvotes", "received downvotes", "received-downvotes", "downvotes", "downvoted")
|
||||
_lb_given_upvotes_meta = LeaderboardMeta("Upvotes", "given upvotes", "given-upvotes", "upvotes", "upvoting")
|
||||
|
||||
def leaderboard_thread():
|
||||
global lb_downvotes_received, lb_upvotes_given
|
||||
|
||||
db:scoped_session = db_session() # type: ignore
|
||||
|
||||
lb_downvotes_received = ReceivedDownvotesLeaderboard(_lb_received_downvotes_meta, db)
|
||||
lb_upvotes_given = GivenUpvotesLeaderboard(_lb_given_upvotes_meta, db)
|
||||
|
||||
db.close()
|
||||
sys.stdout.flush()
|
||||
|
||||
if app.config["ENABLE_SERVICES"]:
|
||||
gevent.spawn(leaderboard_thread())
|
|
@ -25,7 +25,7 @@ def get_logged_in_user():
|
|||
lo_user = session.get("lo_user")
|
||||
if lo_user:
|
||||
id = int(lo_user)
|
||||
v = g.db.query(User).get(id)
|
||||
v = g.db.get(User, id)
|
||||
if v:
|
||||
v.client = None
|
||||
nonce = session.get("login_nonce", 0)
|
||||
|
|
|
@ -14,7 +14,8 @@ from .static import *
|
|||
from .users import *
|
||||
from .votes import *
|
||||
from .feeds import *
|
||||
from .awards import *
|
||||
if FEATURES['AWARDS']:
|
||||
from .awards import * # disable entirely pending possible future use of coins
|
||||
from .volunteer import *
|
||||
if app.debug:
|
||||
from .dev import *
|
||||
|
|
|
@ -1,13 +1,11 @@
|
|||
import time
|
||||
from os import remove
|
||||
from PIL import Image as IMAGE
|
||||
|
||||
from files.helpers.wrappers import *
|
||||
from files.helpers.alerts import *
|
||||
from files.helpers.sanitize import *
|
||||
from files.helpers.security import *
|
||||
from files.helpers.get import *
|
||||
from files.helpers.images import *
|
||||
from files.helpers.media import *
|
||||
from files.helpers.const import *
|
||||
from files.classes import *
|
||||
from flask import *
|
||||
|
@ -16,7 +14,6 @@ from .front import frontlist
|
|||
from files.helpers.comments import comment_on_publish, comment_on_unpublish
|
||||
from datetime import datetime
|
||||
import requests
|
||||
from urllib.parse import quote, urlencode
|
||||
|
||||
month = datetime.now().strftime('%B')
|
||||
|
||||
|
@ -276,12 +273,12 @@ def update_filter_status(v):
|
|||
return { 'result': f'Status of {new_status} is not permitted' }
|
||||
|
||||
if post_id:
|
||||
p = g.db.query(Submission).get(post_id)
|
||||
p = g.db.get(Submission, post_id)
|
||||
old_status = p.filter_state
|
||||
rows_updated = g.db.query(Submission).where(Submission.id == post_id) \
|
||||
.update({Submission.filter_state: new_status})
|
||||
elif comment_id:
|
||||
c = g.db.query(Comment).get(comment_id)
|
||||
c = g.db.get(Comment, comment_id)
|
||||
old_status = c.filter_state
|
||||
rows_updated = g.db.query(Comment).where(Comment.id == comment_id) \
|
||||
.update({Comment.filter_state: new_status})
|
||||
|
@ -414,7 +411,7 @@ def change_settings(v, setting):
|
|||
parent_submission=None,
|
||||
level=1,
|
||||
body_html=body_html,
|
||||
sentto=2,
|
||||
sentto=MODMAIL_ID,
|
||||
distinguish_level=6
|
||||
)
|
||||
g.db.add(new_comment)
|
||||
|
@ -735,13 +732,12 @@ def alt_votes_get(v):
|
|||
@limiter.exempt
|
||||
@admin_level_required(2)
|
||||
def admin_link_accounts(v):
|
||||
|
||||
u1 = int(request.values.get("u1"))
|
||||
u2 = int(request.values.get("u2"))
|
||||
u1 = get_account(request.values.get("u1", ''))
|
||||
u2 = get_account(request.values.get("u2", ''))
|
||||
|
||||
new_alt = Alt(
|
||||
user1=u1,
|
||||
user2=u2,
|
||||
user1=u1.id,
|
||||
user2=u2.id,
|
||||
is_manual=True
|
||||
)
|
||||
|
||||
|
@ -756,7 +752,7 @@ def admin_link_accounts(v):
|
|||
g.db.add(ma)
|
||||
|
||||
g.db.commit()
|
||||
return redirect(f"/admin/alt_votes?u1={g.db.query(User).get(u1).username}&u2={g.db.query(User).get(u2).username}")
|
||||
return redirect(f"/admin/alt_votes?u1={u1.id}&u2={u2.id}")
|
||||
|
||||
|
||||
@app.get("/admin/removed/posts")
|
||||
|
@ -1225,10 +1221,9 @@ def sticky_post(post_id, v):
|
|||
@limiter.exempt
|
||||
@admin_level_required(2)
|
||||
def unsticky_post(post_id, v):
|
||||
|
||||
post = g.db.query(Submission).filter_by(id=post_id).one_or_none()
|
||||
if post and post.stickied:
|
||||
if post.stickied.endswith('(pin award)'): abort(403, "Can't unpin award pins!")
|
||||
if FEATURES['AWARDS'] and post.stickied.endswith('(pin award)'): abort(403, "Can't unpin award pins!")
|
||||
|
||||
post.stickied = None
|
||||
post.stickied_utc = None
|
||||
|
@ -1252,7 +1247,6 @@ def unsticky_post(post_id, v):
|
|||
@limiter.exempt
|
||||
@admin_level_required(2)
|
||||
def sticky_comment(cid, v):
|
||||
|
||||
comment = get_comment(cid, v=v)
|
||||
|
||||
if not comment.is_pinned:
|
||||
|
@ -1278,11 +1272,10 @@ def sticky_comment(cid, v):
|
|||
@limiter.exempt
|
||||
@admin_level_required(2)
|
||||
def unsticky_comment(cid, v):
|
||||
|
||||
comment = get_comment(cid, v=v)
|
||||
|
||||
if comment.is_pinned:
|
||||
if comment.is_pinned.endswith("(pin award)"): abort(403, "Can't unpin award pins!")
|
||||
if FEATURES['AWARDS'] and comment.is_pinned.endswith("(pin award)"): abort(403, "Can't unpin award pins!")
|
||||
|
||||
comment.is_pinned = None
|
||||
g.db.add(comment)
|
||||
|
|
|
@ -15,7 +15,7 @@ from copy import deepcopy
|
|||
def shop(v):
|
||||
abort(404) # disable entirely pending possible future use of coins
|
||||
|
||||
AWARDS = deepcopy(AWARDS2)
|
||||
AWARDS = deepcopy(AWARDS_ENABLED)
|
||||
|
||||
for val in AWARDS.values(): val["owned"] = 0
|
||||
|
||||
|
@ -41,7 +41,7 @@ def buy(v, award):
|
|||
if award == 'ghost' and v.admin_level < 2:
|
||||
abort(403, "Only admins can buy that award.")
|
||||
|
||||
AWARDS = deepcopy(AWARDS2)
|
||||
AWARDS = deepcopy(AWARDS_ENABLED)
|
||||
|
||||
if award not in AWARDS: abort(400)
|
||||
og_price = AWARDS[award]["price"]
|
||||
|
@ -50,7 +50,6 @@ def buy(v, award):
|
|||
|
||||
if request.values.get("mb"):
|
||||
if v.procoins < price: abort(400, "Not enough marseybux.")
|
||||
if award == "grass": abort(403, "You can't buy the grass award with marseybux.")
|
||||
v.procoins -= price
|
||||
else:
|
||||
if v.coins < price: abort(400, "Not enough coins.")
|
||||
|
@ -85,33 +84,8 @@ def buy(v, award):
|
|||
g.db.add(v)
|
||||
|
||||
|
||||
if award == "lootbox":
|
||||
send_repeatable_notification(995, f"@{v.username} bought a lootbox!")
|
||||
for i in [1,2,3,4,5]:
|
||||
award = random.choice(["snow", "gingerbread", "lights", "candycane", "fireplace"])
|
||||
award = AwardRelationship(user_id=v.id, kind=award)
|
||||
g.db.add(award)
|
||||
g.db.flush()
|
||||
v.lootboxes_bought += 1
|
||||
if v.lootboxes_bought == 10 and not v.has_badge(76):
|
||||
new_badge = Badge(badge_id=76, user_id=v.id)
|
||||
g.db.add(new_badge)
|
||||
g.db.flush()
|
||||
send_notification(v.id, f"@AutoJanny has given you the following profile badge:\n\n\n\n{new_badge.name}")
|
||||
elif v.lootboxes_bought == 50 and not v.has_badge(77):
|
||||
new_badge = Badge(badge_id=77, user_id=v.id)
|
||||
g.db.add(new_badge)
|
||||
g.db.flush()
|
||||
send_notification(v.id, f"@AutoJanny has given you the following profile badge:\n\n\n\n{new_badge.name}")
|
||||
elif v.lootboxes_bought == 150 and not v.has_badge(78):
|
||||
new_badge = Badge(badge_id=78, user_id=v.id)
|
||||
g.db.add(new_badge)
|
||||
g.db.flush()
|
||||
send_notification(v.id, f"@AutoJanny has given you the following profile badge:\n\n\n\n{new_badge.name}")
|
||||
|
||||
else:
|
||||
award_object = AwardRelationship(user_id=v.id, kind=award)
|
||||
g.db.add(award_object)
|
||||
award_object = AwardRelationship(user_id=v.id, kind=award)
|
||||
g.db.add(award_object)
|
||||
|
||||
g.db.add(v)
|
||||
g.db.commit()
|
||||
|
@ -161,54 +135,6 @@ def award_post(pid, v):
|
|||
if note: msg += f"\n\n> {note}"
|
||||
send_repeatable_notification(author.id, msg)
|
||||
|
||||
if kind == "ban":
|
||||
link = f"[this post]({post.shortlink})"
|
||||
|
||||
if not author.is_suspended:
|
||||
author.ban(reason=f"1-Day ban award used by @{v.username} on /post/{post.id}", days=1)
|
||||
send_repeatable_notification(author.id, f"Your account has been banned for **a day** for {link}. It sucked and you should feel bad.")
|
||||
elif author.unban_utc:
|
||||
author.unban_utc += 86400
|
||||
send_repeatable_notification(author.id, f"Your account has been banned for **yet another day** for {link}. Seriously man?")
|
||||
elif kind == "unban":
|
||||
if not author.is_suspended or not author.unban_utc or time.time() > author.unban_utc: abort(403)
|
||||
|
||||
if author.unban_utc - time.time() > 86400:
|
||||
author.unban_utc -= 86400
|
||||
send_repeatable_notification(author.id, "Your ban duration has been reduced by 1 day!")
|
||||
else:
|
||||
author.unban_utc = 0
|
||||
author.is_banned = 0
|
||||
author.ban_evade = 0
|
||||
send_repeatable_notification(author.id, "You have been unbanned!")
|
||||
elif kind == "pin":
|
||||
if post.stickied and post.stickied_utc:
|
||||
post.stickied_utc += 3600
|
||||
else:
|
||||
post.stickied = f'{v.username} (pin award)'
|
||||
post.stickied_utc = int(time.time()) + 3600
|
||||
g.db.add(post)
|
||||
cache.delete_memoized(frontlist)
|
||||
elif kind == "unpin":
|
||||
if not post.stickied_utc: abort(403)
|
||||
t = post.stickied_utc - 3600
|
||||
if time.time() > t:
|
||||
post.stickied = None
|
||||
post.stickied_utc = None
|
||||
cache.delete_memoized(frontlist)
|
||||
else: post.stickied_utc = t
|
||||
g.db.add(post)
|
||||
elif kind == "benefactor":
|
||||
author.patron = 1
|
||||
if author.patron_utc: author.patron_utc += 2629746
|
||||
else: author.patron_utc = int(time.time()) + 2629746
|
||||
author.procoins += 2500
|
||||
if not v.has_badge(103):
|
||||
badge = Badge(user_id=v.id, badge_id=103)
|
||||
g.db.add(badge)
|
||||
g.db.flush()
|
||||
send_notification(v.id, f"@AutoJanny has given you the following profile badge:\n\n\n\n{badge.name}")
|
||||
|
||||
if author.received_award_count: author.received_award_count += 1
|
||||
else: author.received_award_count = 1
|
||||
g.db.add(author)
|
||||
|
@ -260,54 +186,6 @@ def award_comment(cid, v):
|
|||
if note: msg += f"\n\n> {note}"
|
||||
send_repeatable_notification(author.id, msg)
|
||||
|
||||
if kind == "benefactor" and author.id == v.id:
|
||||
abort(400, "You can't use this award on yourself.")
|
||||
|
||||
if kind == "ban":
|
||||
link = f"[this comment]({c.shortlink})"
|
||||
|
||||
if not author.is_suspended:
|
||||
author.ban(reason=f"1-Day ban award used by @{v.username} on /comment/{c.id}", days=1)
|
||||
send_repeatable_notification(author.id, f"Your account has been banned for **a day** for {link}. It sucked and you should feel bad.")
|
||||
elif author.unban_utc:
|
||||
author.unban_utc += 86400
|
||||
send_repeatable_notification(author.id, f"Your account has been banned for **yet another day** for {link}. Seriously man?")
|
||||
elif kind == "unban":
|
||||
if not author.is_suspended or not author.unban_utc or time.time() > author.unban_utc: abort(403)
|
||||
|
||||
if author.unban_utc - time.time() > 86400:
|
||||
author.unban_utc -= 86400
|
||||
send_repeatable_notification(author.id, "Your ban duration has been reduced by 1 day!")
|
||||
else:
|
||||
author.unban_utc = 0
|
||||
author.is_banned = 0
|
||||
author.ban_evade = 0
|
||||
send_repeatable_notification(author.id, "You have been unbanned!")
|
||||
elif kind == "pin":
|
||||
if c.is_pinned and c.is_pinned_utc: c.is_pinned_utc += 3600
|
||||
else:
|
||||
c.is_pinned = f'{v.username} (pin award)'
|
||||
c.is_pinned_utc = int(time.time()) + 3600
|
||||
g.db.add(c)
|
||||
elif kind == "unpin":
|
||||
if not c.is_pinned_utc: abort(403)
|
||||
t = c.is_pinned_utc - 3600
|
||||
if time.time() > t:
|
||||
c.is_pinned = None
|
||||
c.is_pinned_utc = None
|
||||
else: c.is_pinned_utc = t
|
||||
g.db.add(c)
|
||||
elif kind == "benefactor":
|
||||
author.patron = 1
|
||||
if author.patron_utc: author.patron_utc += 2629746
|
||||
else: author.patron_utc = int(time.time()) + 2629746
|
||||
author.procoins += 2500
|
||||
if not v.has_badge(103):
|
||||
badge = Badge(user_id=v.id, badge_id=103)
|
||||
g.db.add(badge)
|
||||
g.db.flush()
|
||||
send_notification(v.id, f"@AutoJanny has given you the following profile badge:\n\n\n\n{badge.name}")
|
||||
|
||||
if author.received_award_count: author.received_award_count += 1
|
||||
else: author.received_award_count = 1
|
||||
g.db.add(author)
|
||||
|
@ -323,7 +201,7 @@ def admin_userawards_get(v):
|
|||
abort(404) # disable entirely pending possible future use of coins
|
||||
|
||||
if v.admin_level != 3:
|
||||
return render_template("admin/awards.html", awards=list(AWARDS3.values()), v=v)
|
||||
return render_template("admin/awards.html", awards=list(AWARDS_JL2_PRINTABLE.values()), v=v)
|
||||
|
||||
return render_template("admin/awards.html", awards=list(AWARDS.values()), v=v)
|
||||
|
||||
|
@ -335,22 +213,15 @@ def admin_userawards_post(v):
|
|||
|
||||
try: u = request.values.get("username").strip()
|
||||
except: abort(404)
|
||||
|
||||
whitelist = ("shit", "fireflies", "train", "scooter", "wholesome", "glowie")
|
||||
|
||||
whitelist = ()
|
||||
u = get_user(u, graceful=False, v=v)
|
||||
|
||||
notify_awards = {}
|
||||
|
||||
for key, value in request.values.items():
|
||||
if key not in AWARDS: continue
|
||||
|
||||
if v.admin_level < 3 and key not in whitelist: continue
|
||||
|
||||
if value:
|
||||
|
||||
if int(value) > 10: abort(403)
|
||||
|
||||
if int(value): notify_awards[key] = int(value)
|
||||
|
||||
for x in range(int(value)):
|
||||
|
@ -358,7 +229,6 @@ def admin_userawards_post(v):
|
|||
user_id=u.id,
|
||||
kind=key
|
||||
)
|
||||
|
||||
g.db.add(award)
|
||||
|
||||
if v.id != u.id:
|
||||
|
@ -384,5 +254,9 @@ def admin_userawards_post(v):
|
|||
|
||||
g.db.commit()
|
||||
|
||||
if v.admin_level != 3: return render_template("admin/awards.html", awards=list(AWARDS3.values()), v=v)
|
||||
return render_template("admin/awards.html", awards=list(AWARDS.values()), v=v)
|
||||
if v.admin_level < 3:
|
||||
awards: dict = AWARDS_JL2_PRINTABLE
|
||||
else:
|
||||
awards: dict = AWARDS
|
||||
|
||||
return render_template("admin/awards.html", awards=awards, v=v)
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
from files.helpers.wrappers import *
|
||||
from files.helpers.alerts import *
|
||||
from files.helpers.images import *
|
||||
from files.helpers.media import process_image
|
||||
from files.helpers.const import *
|
||||
from files.helpers.comments import comment_on_publish
|
||||
from files.classes import *
|
||||
|
@ -110,7 +110,7 @@ def post_pid_comment_cid(cid, pid=None, anything=None, v=None, sub=None):
|
|||
def api_comment(v):
|
||||
if v.is_suspended: abort(403, "You can't perform this action while banned.")
|
||||
|
||||
parent_fullname = request.values.get("parent_fullname").strip()
|
||||
parent_fullname = request.values.get("parent_fullname", "").strip()
|
||||
|
||||
if len(parent_fullname) < 4: abort(400)
|
||||
id = parent_fullname[3:]
|
||||
|
@ -129,9 +129,9 @@ def api_comment(v):
|
|||
if not parent_post: abort(404) # don't allow sending comments to the ether
|
||||
level = 1 if isinstance(parent, Submission) else parent.level + 1
|
||||
|
||||
body = request.values.get("body", "").strip()[:10000]
|
||||
|
||||
if not body and not request.files.get('file'): abort(400, "You need to actually write something!")
|
||||
body = sanitize_raw(request.values.get("body"), allow_newlines=True, length_limit=10000)
|
||||
if not body and not request.files.get('file'):
|
||||
abort(400, "You need to actually write something!")
|
||||
|
||||
if request.files.get("file") and request.headers.get("cf-ipcountry") != "T1":
|
||||
files = request.files.getlist('file')[:4]
|
||||
|
@ -147,22 +147,7 @@ def api_comment(v):
|
|||
body += f"\n\n"
|
||||
else:
|
||||
body += f'\n\n<a href="{image}">{image}</a>'
|
||||
elif file.content_type.startswith('video/'):
|
||||
file.save("video.mp4")
|
||||
with open("video.mp4", 'rb') as f:
|
||||
try: req = requests.request("POST", "https://api.imgur.com/3/upload", headers={'Authorization': f'Client-ID {IMGUR_KEY}'}, files=[('video', f)], timeout=5).json()['data']
|
||||
except requests.Timeout: abort(500, "Video upload timed out, please try again!")
|
||||
try: url = req['link']
|
||||
except:
|
||||
error = req['error']
|
||||
if error == 'File exceeds max duration': error += ' (60 seconds)'
|
||||
abort(400, error)
|
||||
if url.endswith('.'): url += 'mp4'
|
||||
if app.config['MULTIMEDIA_EMBEDDING_ENABLED']:
|
||||
body += f"\n\n{url}"
|
||||
else:
|
||||
body += f'\n\n<a href="{url}">{url}</a>'
|
||||
else: abort(400, "Image/Video files only")
|
||||
else: abort(400, "Image files only")
|
||||
|
||||
body_html = sanitize(body, comment=True)
|
||||
|
||||
|
@ -269,12 +254,9 @@ def api_comment(v):
|
|||
@limiter.limit("1/second;30/minute;200/hour;1000/day")
|
||||
@auth_required
|
||||
def edit_comment(cid, v):
|
||||
|
||||
c = get_comment(cid, v=v)
|
||||
|
||||
if c.author_id != v.id: abort(403)
|
||||
|
||||
body = request.values.get("body", "").strip()[:10000]
|
||||
body = sanitize_raw(request.values.get("body"), allow_newlines=True, length_limit=10000)
|
||||
|
||||
if len(body) < 1 and not (request.files.get("file") and request.headers.get("cf-ipcountry") != "T1"):
|
||||
abort(400, "You have to actually type something!")
|
||||
|
@ -325,19 +307,7 @@ def edit_comment(cid, v):
|
|||
file.save(name)
|
||||
url = process_image(name)
|
||||
body += f"\n\n"
|
||||
elif file.content_type.startswith('video/'):
|
||||
file.save("video.mp4")
|
||||
with open("video.mp4", 'rb') as f:
|
||||
try: req = requests.request("POST", "https://api.imgur.com/3/upload", headers={'Authorization': f'Client-ID {IMGUR_KEY}'}, files=[('video', f)], timeout=5).json()['data']
|
||||
except requests.Timeout: abort(500, "Video upload timed out, please try again!")
|
||||
try: url = req['link']
|
||||
except:
|
||||
error = req['error']
|
||||
if error == 'File exceeds max duration': error += ' (60 seconds)'
|
||||
abort(400, error)
|
||||
if url.endswith('.'): url += 'mp4'
|
||||
body += f"\n\n{url}"
|
||||
else: abort(400, "Image/Video files only")
|
||||
else: abort(400, "Image files only")
|
||||
|
||||
body_html = sanitize(body, edit=True)
|
||||
|
||||
|
|
|
@ -1,15 +1,18 @@
|
|||
from files.helpers.wrappers import *
|
||||
from flask import request, session
|
||||
from urllib.parse import quote, urlencode
|
||||
import time
|
||||
from files.__main__ import app
|
||||
from http.client import responses
|
||||
from urllib.parse import quote, urlencode
|
||||
|
||||
from flask import g, redirect, render_template, request, session
|
||||
|
||||
from files.helpers.const import ERROR_MESSAGES, SITE_FULL, WERKZEUG_ERROR_DESCRIPTIONS
|
||||
from files.__main__ import app
|
||||
|
||||
@app.errorhandler(400)
|
||||
@app.errorhandler(401)
|
||||
@app.errorhandler(403)
|
||||
@app.errorhandler(404)
|
||||
@app.errorhandler(405)
|
||||
@app.errorhandler(409)
|
||||
@app.errorhandler(413)
|
||||
@app.errorhandler(422)
|
||||
@app.errorhandler(429)
|
||||
|
|
|
@ -1,9 +1,13 @@
|
|||
from sqlalchemy.orm import Query
|
||||
|
||||
from files.helpers.wrappers import *
|
||||
from files.helpers.get import *
|
||||
from files.helpers.strings import sql_ilike_clean
|
||||
from files.__main__ import app, cache, limiter
|
||||
from files.classes.submission import Submission
|
||||
from files.helpers.contentsorting import apply_time_filter, sort_objects
|
||||
from files.helpers.comments import comment_filter_moderated
|
||||
from files.helpers.contentsorting import \
|
||||
apply_time_filter, sort_objects, sort_comment_results
|
||||
|
||||
defaulttimefilter = environ.get("DEFAULT_TIME_FILTER", "all").strip()
|
||||
|
||||
|
@ -47,7 +51,7 @@ def notifications(v):
|
|||
posts = request.values.get('posts')
|
||||
reddit = request.values.get('reddit')
|
||||
if modmail and v.admin_level > 1:
|
||||
comments = g.db.query(Comment).filter(Comment.sentto==2).order_by(Comment.id.desc()).offset(25*(page-1)).limit(26).all()
|
||||
comments = g.db.query(Comment).filter(Comment.sentto == MODMAIL_ID).order_by(Comment.id.desc()).offset(25*(page-1)).limit(26).all()
|
||||
next_exists = (len(comments) > 25)
|
||||
listing = comments[:25]
|
||||
elif messages:
|
||||
|
@ -347,9 +351,7 @@ def changeloglist(v=None, sort="new", page=1, t="all", site=None):
|
|||
|
||||
|
||||
@app.get("/random_post")
|
||||
@auth_desired
|
||||
def random_post(v):
|
||||
|
||||
def random_post():
|
||||
p = g.db.query(Submission.id).filter(Submission.deleted_utc == 0, Submission.is_banned == False, Submission.private == False).order_by(func.random()).first()
|
||||
|
||||
if p: p = p[0]
|
||||
|
@ -359,8 +361,7 @@ def random_post(v):
|
|||
|
||||
|
||||
@app.get("/random_user")
|
||||
@auth_desired
|
||||
def random_user(v):
|
||||
def random_user():
|
||||
u = g.db.query(User.username).order_by(func.random()).first()
|
||||
|
||||
if u: u = u[0]
|
||||
|
@ -372,27 +373,31 @@ def random_user(v):
|
|||
@app.get("/comments")
|
||||
@auth_required
|
||||
def all_comments(v):
|
||||
try: page = max(int(request.values.get("page", 1)), 1)
|
||||
except: page = 1
|
||||
|
||||
sort=request.values.get("sort", "new")
|
||||
t=request.values.get("t", defaulttimefilter)
|
||||
|
||||
try: gt=int(request.values.get("after", 0))
|
||||
except: gt=0
|
||||
|
||||
try: lt=int(request.values.get("before", 0))
|
||||
except: lt=0
|
||||
|
||||
idlist = get_comments_idlist(v=v, page=page, sort=sort, t=t, gt=gt, lt=lt)
|
||||
comments = get_comments(idlist, v=v)
|
||||
page = max(request.values.get("page", 1, int), 1)
|
||||
sort = request.values.get("sort", "new")
|
||||
time_filter = request.values.get("t", defaulttimefilter)
|
||||
time_gt = request.values.get("after", 0, int)
|
||||
time_lt = request.values.get("before", 0, int)
|
||||
|
||||
idlist = get_comments_idlist(v=v,
|
||||
page=page, sort=sort, t=time_filter, gt=time_gt, lt=time_lt)
|
||||
next_exists = len(idlist) > 25
|
||||
|
||||
idlist = idlist[:25]
|
||||
|
||||
if request.headers.get("Authorization"): return {"data": [x.json for x in comments]}
|
||||
return render_template("home_comments.html", v=v, sort=sort, t=t, page=page, comments=comments, standalone=True, next_exists=next_exists)
|
||||
def comment_tree_filter(q: Query) -> Query:
|
||||
q = q.filter(Comment.id.in_(idlist))
|
||||
q = comment_filter_moderated(q, v)
|
||||
q = q.options(selectinload(Comment.post)) # used for post titles
|
||||
return q
|
||||
|
||||
comments, _ = get_comment_trees_eager(comment_tree_filter, sort=sort, v=v)
|
||||
comments = sort_comment_results(comments, sort=sort, pins=False)
|
||||
|
||||
if request.headers.get("Authorization"):
|
||||
return {"data": [x.json for x in comments]}
|
||||
return render_template("home_comments.html", v=v,
|
||||
sort=sort, t=time_filter, page=page, next_exists=next_exists,
|
||||
comments=comments, standalone=True)
|
||||
|
||||
|
||||
def get_comments_idlist(page=1, v=None, sort="new", t="all", gt=0, lt=0):
|
||||
|
|
|
@ -2,12 +2,11 @@ from urllib.parse import urlencode
|
|||
from files.mail import *
|
||||
from files.__main__ import app, limiter
|
||||
from files.helpers.const import *
|
||||
import requests
|
||||
from files.helpers.captcha import validate_captcha
|
||||
|
||||
@app.get("/login")
|
||||
@auth_desired
|
||||
def login_get(v):
|
||||
|
||||
redir = request.values.get("redirect")
|
||||
if redir:
|
||||
redir = redir.replace("/logged_out", "").strip()
|
||||
|
@ -289,21 +288,11 @@ def sign_up_post(v):
|
|||
|
||||
if existing_account:
|
||||
return signup_error("An account with that username already exists.")
|
||||
|
||||
if app.config.get("HCAPTCHA_SITEKEY"):
|
||||
token = request.values.get("h-captcha-response")
|
||||
if not token:
|
||||
return signup_error("Unable to verify captcha [1].")
|
||||
|
||||
data = {"secret": app.config["HCAPTCHA_SECRET"],
|
||||
"response": token,
|
||||
"sitekey": app.config["HCAPTCHA_SITEKEY"]}
|
||||
url = "https://hcaptcha.com/siteverify"
|
||||
|
||||
x = requests.post(url, data=data, timeout=5)
|
||||
|
||||
if not x.json()["success"]:
|
||||
return signup_error("Unable to verify captcha [2].")
|
||||
|
||||
if not validate_captcha(app.config.get("HCAPTCHA_SECRET", ""),
|
||||
app.config.get("HCAPTCHA_SITEKEY", ""),
|
||||
request.values.get("h-captcha-response", "")):
|
||||
return signup_error("Unable to verify CAPTCHA")
|
||||
|
||||
session.pop("signup_token")
|
||||
|
||||
|
|
|
@ -61,7 +61,7 @@ def request_api_keys(v):
|
|||
parent_submission=None,
|
||||
level=1,
|
||||
body_html=body_html,
|
||||
sentto=2,
|
||||
sentto=MODMAIL_ID,
|
||||
distinguish_level=6
|
||||
)
|
||||
g.db.add(new_comment)
|
||||
|
|
|
@ -3,8 +3,10 @@ import gevent
|
|||
from files.helpers.wrappers import *
|
||||
from files.helpers.sanitize import *
|
||||
from files.helpers.alerts import *
|
||||
from files.helpers.comments import comment_filter_moderated
|
||||
from files.helpers.contentsorting import sort_objects
|
||||
from files.helpers.const import *
|
||||
from files.helpers.media import process_image
|
||||
from files.helpers.strings import sql_ilike_clean
|
||||
from files.classes import *
|
||||
from flask import *
|
||||
|
@ -17,6 +19,7 @@ from os import path
|
|||
import requests
|
||||
from shutil import copyfile
|
||||
from sys import stdout
|
||||
from sqlalchemy.orm import Query
|
||||
|
||||
|
||||
snappyquotes = [f':#{x}:' for x in marseys_const2]
|
||||
|
@ -129,102 +132,39 @@ def post_id(pid, anything=None, v=None):
|
|||
|
||||
if post.club and not (v and (v.paid_dues or v.id == post.author_id)): abort(403)
|
||||
|
||||
if v:
|
||||
votes = g.db.query(CommentVote).filter_by(user_id=v.id).subquery()
|
||||
blocking = v.blocking.subquery()
|
||||
blocked = v.blocked.subquery()
|
||||
|
||||
comments = g.db.query(
|
||||
Comment,
|
||||
votes.c.vote_type,
|
||||
blocking.c.target_id,
|
||||
blocked.c.target_id,
|
||||
)
|
||||
|
||||
if not (v and v.shadowbanned) and not (v and v.admin_level > 2):
|
||||
comments = comments.join(User, User.id == Comment.author_id).filter(User.shadowbanned == None)
|
||||
|
||||
if v.admin_level < 2:
|
||||
filter_clause = ((Comment.filter_state != 'filtered') & (Comment.filter_state != 'removed')) | (Comment.author_id == v.id)
|
||||
comments = comments.filter(filter_clause)
|
||||
|
||||
comments=comments.filter(Comment.parent_submission == post.id).join(
|
||||
votes,
|
||||
votes.c.comment_id == Comment.id,
|
||||
isouter=True
|
||||
).join(
|
||||
blocking,
|
||||
blocking.c.target_id == Comment.author_id,
|
||||
isouter=True
|
||||
).join(
|
||||
blocked,
|
||||
blocked.c.user_id == Comment.author_id,
|
||||
isouter=True
|
||||
)
|
||||
|
||||
output = []
|
||||
for c in comments.all():
|
||||
comment = c[0]
|
||||
comment.voted = c[1] or 0
|
||||
comment.is_blocking = c[2] or 0
|
||||
comment.is_blocked = c[3] or 0
|
||||
output.append(comment)
|
||||
|
||||
pinned = [c[0] for c in comments.filter(Comment.is_pinned != None).all()]
|
||||
|
||||
comments = comments.filter(Comment.level == 1, Comment.is_pinned == None)
|
||||
comments = sort_objects(comments, sort, Comment)
|
||||
comments = [c[0] for c in comments.all()]
|
||||
else:
|
||||
pinned = g.db.query(Comment).filter(Comment.parent_submission == post.id, Comment.is_pinned != None).all()
|
||||
|
||||
comments = g.db.query(Comment).join(User, User.id == Comment.author_id).filter(User.shadowbanned == None, Comment.parent_submission == post.id, Comment.level == 1, Comment.is_pinned == None)
|
||||
comments = sort_objects(comments, sort, Comment)
|
||||
|
||||
filter_clause = (Comment.filter_state != 'filtered') & (Comment.filter_state != 'removed')
|
||||
comments = comments.filter(filter_clause)
|
||||
|
||||
comments = comments.all()
|
||||
|
||||
offset = 0
|
||||
ids = set()
|
||||
|
||||
limit = app.config['RESULTS_PER_PAGE_COMMENTS']
|
||||
offset = 0
|
||||
|
||||
if post.comment_count > limit and not request.headers.get("Authorization") and not request.values.get("all"):
|
||||
comments2 = []
|
||||
count = 0
|
||||
if post.created_utc > 1638672040:
|
||||
for comment in comments:
|
||||
comments2.append(comment)
|
||||
ids.add(comment.id)
|
||||
count += g.db.query(Comment.id).filter_by(parent_submission=post.id, top_comment_id=comment.id).count() + 1
|
||||
if count > limit: break
|
||||
else:
|
||||
for comment in comments:
|
||||
comments2.append(comment)
|
||||
ids.add(comment.id)
|
||||
count += g.db.query(Comment.id).filter_by(parent_submission=post.id, parent_comment_id=comment.id).count() + 1
|
||||
if count > limit: break
|
||||
top_comments = g.db.query(Comment.id, Comment.descendant_count).filter(
|
||||
Comment.parent_submission == post.id,
|
||||
Comment.level == 1,
|
||||
).order_by(Comment.is_pinned.desc().nulls_last())
|
||||
top_comments = comment_filter_moderated(top_comments, v)
|
||||
top_comments = sort_objects(top_comments, sort, Comment)
|
||||
|
||||
if len(comments) == len(comments2): offset = 0
|
||||
else: offset = 1
|
||||
comments = comments2
|
||||
pg_top_comment_ids = []
|
||||
pg_comment_qty = 0
|
||||
for tc_id, tc_children_qty in top_comments.all():
|
||||
if pg_comment_qty >= limit:
|
||||
offset = 1
|
||||
break
|
||||
pg_comment_qty += tc_children_qty + 1
|
||||
pg_top_comment_ids.append(tc_id)
|
||||
|
||||
for pin in pinned:
|
||||
if pin.is_pinned_utc and int(time.time()) > pin.is_pinned_utc:
|
||||
pin.is_pinned = None
|
||||
pin.is_pinned_utc = None
|
||||
g.db.add(pin)
|
||||
pinned.remove(pin)
|
||||
def comment_tree_filter(q: Query) -> Query:
|
||||
q = q.filter(Comment.top_comment_id.in_(pg_top_comment_ids))
|
||||
q = comment_filter_moderated(q, v)
|
||||
return q
|
||||
|
||||
top_comments = pinned + comments
|
||||
top_comment_ids = [c.id for c in top_comments]
|
||||
post.replies = get_comment_trees_eager(top_comment_ids, sort, v)
|
||||
comments, comment_tree = get_comment_trees_eager(comment_tree_filter, sort, v)
|
||||
post.replies = comment_tree[None] # parent=None -> top-level comments
|
||||
ids = {c.id for c in post.replies}
|
||||
|
||||
post.views += 1
|
||||
g.db.expire_on_commit = False
|
||||
g.db.add(post)
|
||||
g.db.commit()
|
||||
g.db.expire_on_commit = True
|
||||
|
||||
if request.headers.get("Authorization"): return post.json
|
||||
else:
|
||||
|
@ -239,95 +179,52 @@ def viewmore(v, pid, sort, offset):
|
|||
post = get_post(pid, v=v)
|
||||
if post.club and not (v and (v.paid_dues or v.id == post.author_id)): abort(403)
|
||||
|
||||
offset = int(offset)
|
||||
offset_prev = int(offset)
|
||||
try: ids = set(int(x) for x in request.values.get("ids").split(','))
|
||||
except: abort(400)
|
||||
|
||||
if sort == "new":
|
||||
newest = g.db.query(Comment).filter(Comment.id.in_(ids)).order_by(Comment.created_utc.desc()).first()
|
||||
|
||||
if v:
|
||||
votes = g.db.query(CommentVote).filter_by(user_id=v.id).subquery()
|
||||
|
||||
blocking = v.blocking.subquery()
|
||||
|
||||
blocked = v.blocked.subquery()
|
||||
|
||||
comments = g.db.query(
|
||||
Comment,
|
||||
votes.c.vote_type,
|
||||
blocking.c.target_id,
|
||||
blocked.c.target_id,
|
||||
).filter(Comment.parent_submission == pid, Comment.is_pinned == None, Comment.id.notin_(ids))
|
||||
|
||||
if not (v and v.shadowbanned) and not (v and v.admin_level > 2):
|
||||
comments = comments.join(User, User.id == Comment.author_id).filter(User.shadowbanned == None)
|
||||
|
||||
if not v or v.admin_level < 2:
|
||||
filter_clause = (Comment.filter_state != 'filtered') & (Comment.filter_state != 'removed')
|
||||
if v:
|
||||
filter_clause = filter_clause | (Comment.author_id == v.id)
|
||||
comments = comments.filter(filter_clause)
|
||||
|
||||
comments=comments.join(
|
||||
votes,
|
||||
votes.c.comment_id == Comment.id,
|
||||
isouter=True
|
||||
).join(
|
||||
blocking,
|
||||
blocking.c.target_id == Comment.author_id,
|
||||
isouter=True
|
||||
).join(
|
||||
blocked,
|
||||
blocked.c.user_id == Comment.author_id,
|
||||
isouter=True
|
||||
)
|
||||
|
||||
output = []
|
||||
for c in comments.all():
|
||||
comment = c[0]
|
||||
comment.voted = c[1] or 0
|
||||
comment.is_blocking = c[2] or 0
|
||||
comment.is_blocked = c[3] or 0
|
||||
output.append(comment)
|
||||
|
||||
comments = comments.filter(Comment.level == 1)
|
||||
|
||||
if sort == "new":
|
||||
comments = comments.filter(Comment.created_utc < newest.created_utc)
|
||||
comments = sort_objects(comments, sort, Comment)
|
||||
|
||||
comments = [c[0] for c in comments.all()]
|
||||
else:
|
||||
comments = g.db.query(Comment).join(User, User.id == Comment.author_id).filter(User.shadowbanned == None, Comment.parent_submission == pid, Comment.level == 1, Comment.is_pinned == None, Comment.id.notin_(ids))
|
||||
|
||||
if sort == "new":
|
||||
comments = comments.filter(Comment.created_utc < newest.created_utc)
|
||||
comments = sort_objects(comments, sort, Comment)
|
||||
|
||||
comments = comments.all()
|
||||
comments = comments[offset:]
|
||||
|
||||
limit = app.config['RESULTS_PER_PAGE_COMMENTS']
|
||||
comments2 = []
|
||||
count = 0
|
||||
offset = 0
|
||||
|
||||
if post.created_utc > 1638672040:
|
||||
for comment in comments:
|
||||
comments2.append(comment)
|
||||
ids.add(comment.id)
|
||||
count += g.db.query(Comment.id).filter_by(parent_submission=post.id, top_comment_id=comment.id).count() + 1
|
||||
if count > limit: break
|
||||
else:
|
||||
for comment in comments:
|
||||
comments2.append(comment)
|
||||
ids.add(comment.id)
|
||||
count += g.db.query(Comment.id).filter_by(parent_submission=post.id, parent_comment_id=comment.id).count() + 1
|
||||
if count > limit: break
|
||||
|
||||
if len(comments) == len(comments2): offset = 0
|
||||
else: offset += 1
|
||||
comments = comments2
|
||||
# TODO: Unify with common post_id logic
|
||||
top_comments = g.db.query(Comment.id, Comment.descendant_count).filter(
|
||||
Comment.parent_submission == post.id,
|
||||
Comment.level == 1,
|
||||
Comment.id.notin_(ids),
|
||||
Comment.is_pinned == None,
|
||||
).order_by(Comment.is_pinned.desc().nulls_last())
|
||||
|
||||
if sort == "new":
|
||||
newest_created_utc = g.db.query(Comment.created_utc).filter(
|
||||
Comment.id.in_(ids),
|
||||
Comment.is_pinned == None,
|
||||
).order_by(Comment.created_utc.desc()).limit(1).scalar()
|
||||
|
||||
# Needs to be <=, not just <, to support seed_db data which has many identical
|
||||
# created_utc values. Shouldn't cause duplication in real data because of the
|
||||
# `NOT IN :ids` in top_comments.
|
||||
top_comments = top_comments.filter(Comment.created_utc <= newest_created_utc)
|
||||
|
||||
top_comments = comment_filter_moderated(top_comments, v)
|
||||
top_comments = sort_objects(top_comments, sort, Comment)
|
||||
|
||||
pg_top_comment_ids = []
|
||||
pg_comment_qty = 0
|
||||
for tc_id, tc_children_qty in top_comments.all():
|
||||
if pg_comment_qty >= limit:
|
||||
offset = offset_prev + 1
|
||||
break
|
||||
pg_comment_qty += tc_children_qty + 1
|
||||
pg_top_comment_ids.append(tc_id)
|
||||
|
||||
def comment_tree_filter(q: Query) -> Query:
|
||||
q = q.filter(Comment.top_comment_id.in_(pg_top_comment_ids))
|
||||
q = comment_filter_moderated(q, v)
|
||||
return q
|
||||
|
||||
_, comment_tree = get_comment_trees_eager(comment_tree_filter, sort, v)
|
||||
comments = comment_tree[None] # parent=None -> top-level comments
|
||||
ids |= {c.id for c in comments}
|
||||
|
||||
return render_template("comments.html", v=v, comments=comments, p=post, ids=list(ids), render_replies=True, pid=pid, sort=sort, offset=offset, ajax=True)
|
||||
|
||||
|
@ -353,7 +250,7 @@ def morecomments(v, cid):
|
|||
votes.c.vote_type,
|
||||
blocking.c.target_id,
|
||||
blocked.c.target_id,
|
||||
).filter(Comment.top_comment_id == tcid, Comment.level > 9).join(
|
||||
).filter(Comment.top_comment_id == tcid, Comment.level > RENDER_DEPTH_LIMIT).join(
|
||||
votes,
|
||||
votes.c.comment_id == Comment.id,
|
||||
isouter=True
|
||||
|
@ -396,7 +293,10 @@ def edit_post(pid, v):
|
|||
if p.author_id != v.id and not (v.admin_level > 1 and v.admin_level > 2): abort(403)
|
||||
|
||||
title = guarded_value("title", 1, MAX_TITLE_LENGTH)
|
||||
title = sanitize_raw(title, allow_newlines=False, length_limit=MAX_TITLE_LENGTH)
|
||||
|
||||
body = guarded_value("body", 0, MAX_BODY_LENGTH)
|
||||
body = sanitize_raw(body, allow_newlines=True, length_limit=MAX_BODY_LENGTH)
|
||||
|
||||
if title != p.title:
|
||||
p.title = title
|
||||
|
@ -414,22 +314,7 @@ def edit_post(pid, v):
|
|||
body += f"\n\n"
|
||||
else:
|
||||
body += f'\n\n<a href="{url}">{url}</a>'
|
||||
elif file.content_type.startswith('video/'):
|
||||
file.save("video.mp4")
|
||||
with open("video.mp4", 'rb') as f:
|
||||
try: req = requests.request("POST", "https://api.imgur.com/3/upload", headers={'Authorization': f'Client-ID {IMGUR_KEY}'}, files=[('video', f)], timeout=5).json()['data']
|
||||
except requests.Timeout: abort(500, "Video upload timed out, please try again!")
|
||||
try: url = req['link']
|
||||
except:
|
||||
error = req['error']
|
||||
if error == 'File exceeds max duration': error += ' (60 seconds)'
|
||||
abort(400, error)
|
||||
if url.endswith('.'): url += 'mp4'
|
||||
if app.config['MULTIMEDIA_EMBEDDING_ENABLED']:
|
||||
body += f"\n\n"
|
||||
else:
|
||||
body += f'\n\n<a href="{url}">{url}</a>'
|
||||
else: abort(400, "Image/Video files only")
|
||||
else: abort(400, "Image files only")
|
||||
|
||||
body_html = sanitize(body, edit=True)
|
||||
|
||||
|
@ -663,11 +548,15 @@ def submit_post(v):
|
|||
if request.headers.get("Authorization") or request.headers.get("xhr"): abort(400, error)
|
||||
return render_template("submit.html", v=v, error=error, title=title, url=url, body=body), 400
|
||||
|
||||
title = guarded_value("title", 1, MAX_TITLE_LENGTH)
|
||||
url = guarded_value("url", 0, MAX_URL_LENGTH)
|
||||
body = guarded_value("body", 0, MAX_BODY_LENGTH)
|
||||
|
||||
if v.is_suspended: return error("You can't perform this action while banned.")
|
||||
|
||||
title = guarded_value("title", 1, MAX_TITLE_LENGTH)
|
||||
title = sanitize_raw(title, allow_newlines=False, length_limit=MAX_TITLE_LENGTH)
|
||||
|
||||
url = guarded_value("url", 0, MAX_URL_LENGTH)
|
||||
|
||||
body = guarded_value("body", 0, MAX_BODY_LENGTH)
|
||||
body = sanitize_raw(body, allow_newlines=True, length_limit=MAX_BODY_LENGTH)
|
||||
|
||||
title_html = filter_emojis_only(title, graceful=True)
|
||||
|
||||
|
@ -819,23 +708,8 @@ def submit_post(v):
|
|||
body += f"\n\n"
|
||||
else:
|
||||
body += f'\n\n<a href="{image}">{image}</a>'
|
||||
elif file.content_type.startswith('video/'):
|
||||
file.save("video.mp4")
|
||||
with open("video.mp4", 'rb') as f:
|
||||
try: req = requests.request("POST", "https://api.imgur.com/3/upload", headers={'Authorization': f'Client-ID {IMGUR_KEY}'}, files=[('video', f)], timeout=5).json()['data']
|
||||
except requests.Timeout: return error("Video upload timed out, please try again!")
|
||||
try: url = req['link']
|
||||
except:
|
||||
err = req['error']
|
||||
if err == 'File exceeds max duration': err += ' (60 seconds)'
|
||||
return error(err)
|
||||
if url.endswith('.'): url += 'mp4'
|
||||
if app.config['MULTIMEDIA_EMBEDDING_ENABLED']:
|
||||
body += f"\n\n"
|
||||
else:
|
||||
body += f'\n\n<a href="{url}">{url}</a>'
|
||||
else:
|
||||
return error("Image/Video files only.")
|
||||
return error("Image files only")
|
||||
|
||||
body_html = sanitize(body)
|
||||
|
||||
|
@ -888,21 +762,9 @@ def submit_post(v):
|
|||
|
||||
name2 = name.replace('.webp', 'r.webp')
|
||||
copyfile(name, name2)
|
||||
post.thumburl = process_image(name2, resize=100)
|
||||
elif file.content_type.startswith('video/'):
|
||||
file.save("video.mp4")
|
||||
with open("video.mp4", 'rb') as f:
|
||||
try: req = requests.request("POST", "https://api.imgur.com/3/upload", headers={'Authorization': f'Client-ID {IMGUR_KEY}'}, files=[('video', f)], timeout=5).json()['data']
|
||||
except requests.Timeout: return error("Video upload timed out, please try again!")
|
||||
try: url = req['link']
|
||||
except:
|
||||
err = req['error']
|
||||
if err == 'File exceeds max duration': err += ' (60 seconds)'
|
||||
return error(err)
|
||||
if url.endswith('.'): url += 'mp4'
|
||||
post.url = url
|
||||
post.thumburl = process_image(name2, resize=100)
|
||||
else:
|
||||
return error("Image/Video files only.")
|
||||
return error("Image files only")
|
||||
|
||||
if not post.thumburl and post.url:
|
||||
gevent.spawn(thumbnail_thread, post.id)
|
||||
|
|
|
@ -1,10 +1,9 @@
|
|||
from __future__ import unicode_literals
|
||||
from files.helpers.alerts import *
|
||||
from files.helpers.media import process_image
|
||||
from files.helpers.sanitize import *
|
||||
from files.helpers.const import *
|
||||
from files.mail import *
|
||||
from files.__main__ import app, cache, limiter
|
||||
import youtube_dl
|
||||
from .front import frontlist
|
||||
import os
|
||||
from files.helpers.sanitize import filter_emojis_only
|
||||
|
@ -173,21 +172,9 @@ def settings_profile_post(v):
|
|||
file.save(name)
|
||||
url = process_image(name)
|
||||
bio += f"\n\n"
|
||||
elif file.content_type.startswith('video/'):
|
||||
file.save("video.mp4")
|
||||
with open("video.mp4", 'rb') as f:
|
||||
try: req = requests.request("POST", "https://api.imgur.com/3/upload", headers={'Authorization': f'Client-ID {IMGUR_KEY}'}, files=[('video', f)], timeout=5).json()['data']
|
||||
except requests.Timeout: abort(500, "Video upload timed out, please try again!")
|
||||
try: url = req['link']
|
||||
except:
|
||||
error = req['error']
|
||||
if error == 'File exceeds max duration': error += ' (60 seconds)'
|
||||
abort(400, error)
|
||||
if url.endswith('.'): url += 'mp4'
|
||||
bio += f"\n\n{url}"
|
||||
else:
|
||||
if request.headers.get("Authorization") or request.headers.get("xhr"): abort(400, "Image/Video files only")
|
||||
return render_template("settings_profile.html", v=v, error="Image/Video files only."), 400
|
||||
if request.headers.get("Authorization") or request.headers.get("xhr"): abort(400, "Image files only")
|
||||
return render_template("settings_profile.html", v=v, error="Image files only"), 400
|
||||
|
||||
bio_html = sanitize(bio)
|
||||
|
||||
|
@ -217,14 +204,14 @@ def settings_profile_post(v):
|
|||
|
||||
defaultsortingcomments = request.values.get("defaultsortingcomments")
|
||||
if defaultsortingcomments:
|
||||
if defaultsortingcomments in {"new", "old", "controversial", "top", "bottom"}:
|
||||
if defaultsortingcomments in SORTS_COMMENTS:
|
||||
v.defaultsortingcomments = defaultsortingcomments
|
||||
updated = True
|
||||
else: abort(400)
|
||||
|
||||
defaultsorting = request.values.get("defaultsorting")
|
||||
if defaultsorting:
|
||||
if defaultsorting in {"hot", "bump", "new", "old", "comments", "controversial", "top", "bottom"}:
|
||||
if defaultsorting in SORTS_POSTS:
|
||||
v.defaultsorting = defaultsorting
|
||||
updated = True
|
||||
else: abort(400)
|
||||
|
@ -549,7 +536,6 @@ def settings_profilecss(v):
|
|||
@limiter.limit("1/second;10/day")
|
||||
@auth_required
|
||||
def settings_block_user(v):
|
||||
|
||||
user = get_user(request.values.get("username"), graceful=True)
|
||||
|
||||
if not user: abort(404, "That user doesn't exist.")
|
||||
|
@ -567,11 +553,7 @@ def settings_block_user(v):
|
|||
target_id=user.id,
|
||||
)
|
||||
g.db.add(new_block)
|
||||
|
||||
send_notification(user.id, f"@{v.username} has blocked you!")
|
||||
|
||||
cache.delete_memoized(frontlist)
|
||||
|
||||
g.db.commit()
|
||||
|
||||
return {"message": f"@{user.username} blocked."}
|
||||
|
@ -581,19 +563,11 @@ def settings_block_user(v):
|
|||
@limiter.limit("1/second;30/minute;200/hour;1000/day")
|
||||
@auth_required
|
||||
def settings_unblock_user(v):
|
||||
|
||||
user = get_user(request.values.get("username"))
|
||||
|
||||
x = v.is_blocking(user)
|
||||
|
||||
if not x: abort(409)
|
||||
|
||||
g.db.delete(x)
|
||||
|
||||
send_notification(user.id, f"@{v.username} has unblocked you!")
|
||||
|
||||
cache.delete_memoized(frontlist)
|
||||
|
||||
g.db.commit()
|
||||
|
||||
return {"message": f"@{user.username} unblocked."}
|
||||
|
@ -647,85 +621,6 @@ def settings_name_change(v):
|
|||
|
||||
return redirect("/settings/profile")
|
||||
|
||||
@app.post("/settings/song_change")
|
||||
@limiter.limit("2/second;10/day")
|
||||
@auth_required
|
||||
def settings_song_change(v):
|
||||
song=request.values.get("song").strip()
|
||||
|
||||
if song == "" and v.song:
|
||||
if path.isfile(f"/songs/{v.song}.mp3") and g.db.query(User.id).filter_by(song=v.song).count() == 1:
|
||||
os.remove(f"/songs/{v.song}.mp3")
|
||||
v.song = None
|
||||
g.db.add(v)
|
||||
g.db.commit()
|
||||
return redirect("/settings/profile")
|
||||
|
||||
song = song.replace("https://music.youtube.com", "https://youtube.com")
|
||||
if song.startswith(("https://www.youtube.com/watch?v=", "https://youtube.com/watch?v=", "https://m.youtube.com/watch?v=")):
|
||||
id = song.split("v=")[1]
|
||||
elif song.startswith("https://youtu.be/"):
|
||||
id = song.split("https://youtu.be/")[1]
|
||||
else:
|
||||
return render_template("settings_profile.html", v=v, error="Not a youtube link.")
|
||||
|
||||
if "?" in id: id = id.split("?")[0]
|
||||
if "&" in id: id = id.split("&")[0]
|
||||
|
||||
if path.isfile(f'/songs/{id}.mp3'):
|
||||
v.song = id
|
||||
g.db.add(v)
|
||||
g.db.commit()
|
||||
return redirect("/settings/profile")
|
||||
|
||||
|
||||
req = requests.get(f"https://www.googleapis.com/youtube/v3/videos?id={id}&key={YOUTUBE_KEY}&part=contentDetails", timeout=5).json()
|
||||
duration = req['items'][0]['contentDetails']['duration']
|
||||
if duration == 'P0D':
|
||||
return render_template("settings_profile.html", v=v, error="Can't use a live youtube video!")
|
||||
|
||||
if "H" in duration:
|
||||
return render_template("settings_profile.html", v=v, error="Duration of the video must not exceed 15 minutes.")
|
||||
|
||||
if "M" in duration:
|
||||
duration = int(duration.split("PT")[1].split("M")[0])
|
||||
if duration > 15:
|
||||
return render_template("settings_profile.html", v=v, error="Duration of the video must not exceed 15 minutes.")
|
||||
|
||||
|
||||
if v.song and path.isfile(f"/songs/{v.song}.mp3") and g.db.query(User.id).filter_by(song=v.song).count() == 1:
|
||||
os.remove(f"/songs/{v.song}.mp3")
|
||||
|
||||
ydl_opts = {
|
||||
'outtmpl': '/songs/%(title)s.%(ext)s',
|
||||
'format': 'bestaudio/best',
|
||||
'postprocessors': [{
|
||||
'key': 'FFmpegExtractAudio',
|
||||
'preferredcodec': 'mp3',
|
||||
'preferredquality': '192',
|
||||
}],
|
||||
}
|
||||
|
||||
with youtube_dl.YoutubeDL(ydl_opts) as ydl:
|
||||
try: ydl.download([f"https://youtube.com/watch?v={id}"])
|
||||
except Exception as e:
|
||||
print(e)
|
||||
return render_template("settings_profile.html",
|
||||
v=v,
|
||||
error="Age-restricted videos aren't allowed.")
|
||||
|
||||
files = os.listdir("/songs/")
|
||||
paths = [path.join("/songs/", basename) for basename in files]
|
||||
songfile = max(paths, key=path.getctime)
|
||||
os.rename(songfile, f"/songs/{id}.mp3")
|
||||
|
||||
v.song = id
|
||||
g.db.add(v)
|
||||
|
||||
g.db.commit()
|
||||
|
||||
return redirect("/settings/profile")
|
||||
|
||||
@app.post("/settings/title_change")
|
||||
@limiter.limit("1/second;30/minute;200/hour;1000/day")
|
||||
@auth_required
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
from files.helpers.media import process_image
|
||||
from files.mail import *
|
||||
from files.__main__ import app, limiter, mail
|
||||
from files.helpers.alerts import *
|
||||
from files.helpers.const import *
|
||||
from files.helpers.captcha import validate_captcha
|
||||
from files.classes.award import AWARDS
|
||||
from sqlalchemy import func
|
||||
from os import path
|
||||
|
@ -108,15 +110,13 @@ def chart():
|
|||
|
||||
|
||||
@app.get("/weekly_chart")
|
||||
@auth_desired
|
||||
def weekly_chart(v):
|
||||
def weekly_chart():
|
||||
file = cached_chart(kind="weekly", site=SITE)
|
||||
f = send_file(file)
|
||||
return f
|
||||
|
||||
@app.get("/daily_chart")
|
||||
@auth_desired
|
||||
def daily_chart(v):
|
||||
def daily_chart():
|
||||
file = cached_chart(kind="daily", site=SITE)
|
||||
f = send_file(file)
|
||||
return f
|
||||
|
@ -280,13 +280,17 @@ def api(v):
|
|||
@app.get("/media")
|
||||
@auth_desired
|
||||
def contact(v):
|
||||
|
||||
return render_template("contact.html", v=v)
|
||||
return render_template("contact.html", v=v,
|
||||
hcaptcha=app.config.get("HCAPTCHA_SITEKEY", ""))
|
||||
|
||||
@app.post("/send_admin")
|
||||
@limiter.limit("1/second;2/minute;6/hour;10/day")
|
||||
@auth_desired
|
||||
def submit_contact(v):
|
||||
def submit_contact(v: Optional[User]):
|
||||
if not v and not validate_captcha(app.config.get("HCAPTCHA_SECRET", ""),
|
||||
app.config.get("HCAPTCHA_SITEKEY", ""),
|
||||
request.values.get("h-captcha-response", "")):
|
||||
abort(403, "CAPTCHA provided was not correct. Please try it again")
|
||||
body = request.values.get("message")
|
||||
email = request.values.get("email")
|
||||
if not body: abort(400)
|
||||
|
@ -306,25 +310,13 @@ def submit_contact(v):
|
|||
file.save(name)
|
||||
url = process_image(name)
|
||||
html += f'<img data-bs-target="#expandImageModal" data-bs-toggle="modal" onclick="expandDesktopImage(this.src)" class="img" src="{url}" loading="lazy">'
|
||||
elif file.content_type.startswith('video/'):
|
||||
file.save("video.mp4")
|
||||
with open("video.mp4", 'rb') as f:
|
||||
try: req = requests.request("POST", "https://api.imgur.com/3/upload", headers={'Authorization': f'Client-ID {IMGUR_KEY}'}, files=[('video', f)], timeout=5).json()['data']
|
||||
except requests.Timeout: abort(500, "Video upload timed out, please try again!")
|
||||
try: url = req['link']
|
||||
except:
|
||||
error = req['error']
|
||||
if error == 'File exceeds max duration': error += ' (60 seconds)'
|
||||
abort(400, error)
|
||||
if url.endswith('.'): url += 'mp4'
|
||||
html += f"<p>{url}</p>"
|
||||
else: abort(400, "Image/Video files only")
|
||||
else: abort(400, "Image files only")
|
||||
|
||||
new_comment = Comment(author_id=v.id if v else NOTIFICATIONS_ID,
|
||||
parent_submission=None,
|
||||
level=1,
|
||||
body_html=html,
|
||||
sentto=2
|
||||
sentto=MODMAIL_ID,
|
||||
)
|
||||
g.db.add(new_comment)
|
||||
g.db.flush()
|
||||
|
|
|
@ -2,8 +2,11 @@ import qrcode
|
|||
import io
|
||||
import time
|
||||
import math
|
||||
|
||||
from files.classes.leaderboard import SimpleLeaderboard, BadgeMarseyLeaderboard, UserBlockLeaderboard, LeaderboardMeta
|
||||
from files.classes.views import ViewerRelationship
|
||||
from files.helpers.alerts import *
|
||||
from files.helpers.media import process_image
|
||||
from files.helpers.sanitize import *
|
||||
from files.helpers.strings import sql_ilike_clean
|
||||
from files.helpers.const import *
|
||||
|
@ -11,69 +14,14 @@ from files.helpers.assetcache import assetcache_path
|
|||
from files.helpers.contentsorting import apply_time_filter, sort_objects
|
||||
from files.mail import *
|
||||
from flask import *
|
||||
from files.__main__ import app, limiter, db_session
|
||||
from pusher_push_notifications import PushNotifications
|
||||
from files.__main__ import app, limiter
|
||||
from collections import Counter
|
||||
import gevent
|
||||
from sys import stdout
|
||||
|
||||
if PUSHER_ID != 'blahblahblah':
|
||||
beams_client = PushNotifications(instance_id=PUSHER_ID, secret_key=PUSHER_KEY)
|
||||
|
||||
def pusher_thread2(interests, notifbody, username):
|
||||
beams_client.publish_to_interests(
|
||||
interests=[interests],
|
||||
publish_body={
|
||||
'web': {
|
||||
'notification': {
|
||||
'title': f'New message from @{username}',
|
||||
'body': notifbody,
|
||||
'deep_link': f'{SITE_FULL}/notifications?messages=true',
|
||||
'icon': SITE_FULL + assetcache_path(f'images/{SITE_ID}/icon.webp'),
|
||||
}
|
||||
},
|
||||
'fcm': {
|
||||
'notification': {
|
||||
'title': f'New message from @{username}',
|
||||
'body': notifbody,
|
||||
},
|
||||
'data': {
|
||||
'url': '/notifications?messages=true',
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
stdout.flush()
|
||||
|
||||
def leaderboard_thread():
|
||||
global users9, users9_25, users13, users13_25
|
||||
|
||||
db = db_session()
|
||||
|
||||
votes1 = db.query(Submission.author_id, func.count(Submission.author_id)).join(Vote, Vote.submission_id==Submission.id).filter(Vote.vote_type==-1).group_by(Submission.author_id).order_by(func.count(Submission.author_id).desc()).all()
|
||||
votes2 = db.query(Comment.author_id, func.count(Comment.author_id)).join(CommentVote, CommentVote.comment_id==Comment.id).filter(CommentVote.vote_type==-1).group_by(Comment.author_id).order_by(func.count(Comment.author_id).desc()).all()
|
||||
votes3 = Counter(dict(votes1)) + Counter(dict(votes2))
|
||||
users8 = db.query(User).filter(User.id.in_(votes3.keys())).all()
|
||||
users9 = []
|
||||
for user in users8: users9.append((user, votes3[user.id]))
|
||||
users9 = sorted(users9, key=lambda x: x[1], reverse=True)
|
||||
users9_25 = users9[:25]
|
||||
|
||||
votes1 = db.query(Vote.user_id, func.count(Vote.user_id)).filter(Vote.vote_type==1).group_by(Vote.user_id).order_by(func.count(Vote.user_id).desc()).all()
|
||||
votes2 = db.query(CommentVote.user_id, func.count(CommentVote.user_id)).filter(CommentVote.vote_type==1).group_by(CommentVote.user_id).order_by(func.count(CommentVote.user_id).desc()).all()
|
||||
votes3 = Counter(dict(votes1)) + Counter(dict(votes2))
|
||||
users14 = db.query(User).filter(User.id.in_(votes3.keys())).all()
|
||||
users13 = []
|
||||
for user in users14:
|
||||
users13.append((user, votes3[user.id]-user.post_count-user.comment_count))
|
||||
users13 = sorted(users13, key=lambda x: x[1], reverse=True)
|
||||
users13_25 = users13[:25]
|
||||
|
||||
db.close()
|
||||
stdout.flush()
|
||||
|
||||
if app.config["ENABLE_SERVICES"]:
|
||||
gevent.spawn(leaderboard_thread())
|
||||
# warning: do not move currently. these have import-time side effects but
|
||||
# until this is refactored to be not completely awful, there's not really
|
||||
# a better option.
|
||||
from files.helpers.services import *
|
||||
|
||||
@app.get("/@<username>/upvoters/<uid>/posts")
|
||||
@admin_level_required(3)
|
||||
|
@ -411,73 +359,26 @@ def transfer_bux(v, username):
|
|||
|
||||
@app.get("/leaderboard")
|
||||
@admin_level_required(2)
|
||||
def leaderboard(v):
|
||||
def leaderboard(v:User):
|
||||
users:Query = g.db.query(User)
|
||||
if not v.can_see_shadowbanned:
|
||||
users = users.filter(User.shadowbanned == None)
|
||||
|
||||
users = g.db.query(User)
|
||||
coins = SimpleLeaderboard(v, LeaderboardMeta("Coins", "coins", "coins", "Coins", None), g.db, users, User.coins)
|
||||
subscribers = SimpleLeaderboard(v, LeaderboardMeta("Followers", "followers", "followers", "Followers", "followers"), g.db, users, User.stored_subscriber_count)
|
||||
posts = SimpleLeaderboard(v, LeaderboardMeta("Posts", "post count", "posts", "Posts", ""), g.db, users, User.post_count)
|
||||
comments = SimpleLeaderboard(v, LeaderboardMeta("Comments", "comment count", "comments", "Comments", "comments"), g.db, users, User.comment_count)
|
||||
received_awards = SimpleLeaderboard(v, LeaderboardMeta("Awards", "received awards", "awards", "Awards", None), g.db, users, User.received_award_count)
|
||||
coins_spent = SimpleLeaderboard(v, LeaderboardMeta("Spent in shop", "coins spent in shop", "spent", "Coins", None), g.db, users, User.coins_spent)
|
||||
truescore = SimpleLeaderboard(v, LeaderboardMeta("Truescore", "truescore", "truescore", "Truescore", None), g.db, users, User.truecoins)
|
||||
badges = BadgeMarseyLeaderboard(v, LeaderboardMeta("Badges", "badges", "badges", "Badges", None), g.db, Badge.user_id)
|
||||
blocks = UserBlockLeaderboard(v, LeaderboardMeta("Blocked", "most blocked", "blocked", "Blocked By", "blockers"), g.db, UserBlock.target_id)
|
||||
|
||||
users1 = users.order_by(User.coins.desc()).limit(25).all()
|
||||
sq = g.db.query(User.id, func.rank().over(order_by=User.coins.desc()).label("rank")).subquery()
|
||||
pos1 = g.db.query(sq.c.id, sq.c.rank).filter(sq.c.id == v.id).limit(1).one()[1]
|
||||
# note: lb_downvotes_received and lb_upvotes_given are global variables
|
||||
# that are populated by leaderboard_thread() in files.helpers.services
|
||||
leaderboards = [coins, coins_spent, truescore, subscribers, posts, comments, received_awards, badges, blocks, lb_downvotes_received, lb_upvotes_given]
|
||||
|
||||
users2 = users.order_by(User.stored_subscriber_count.desc()).limit(25).all()
|
||||
sq = g.db.query(User.id, func.rank().over(order_by=User.stored_subscriber_count.desc()).label("rank")).subquery()
|
||||
pos2 = g.db.query(sq.c.id, sq.c.rank).filter(sq.c.id == v.id).limit(1).one()[1]
|
||||
|
||||
users3 = users.order_by(User.post_count.desc()).limit(25).all()
|
||||
sq = g.db.query(User.id, func.rank().over(order_by=User.post_count.desc()).label("rank")).subquery()
|
||||
pos3 = g.db.query(sq.c.id, sq.c.rank).filter(sq.c.id == v.id).limit(1).one()[1]
|
||||
|
||||
users4 = users.order_by(User.comment_count.desc()).limit(25).all()
|
||||
sq = g.db.query(User.id, func.rank().over(order_by=User.comment_count.desc()).label("rank")).subquery()
|
||||
pos4 = g.db.query(sq.c.id, sq.c.rank).filter(sq.c.id == v.id).limit(1).one()[1]
|
||||
|
||||
users5 = users.order_by(User.received_award_count.desc()).limit(25).all()
|
||||
sq = g.db.query(User.id, func.rank().over(order_by=User.received_award_count.desc()).label("rank")).subquery()
|
||||
pos5 = g.db.query(sq.c.id, sq.c.rank).filter(sq.c.id == v.id).limit(1).one()[1]
|
||||
|
||||
users6 = None
|
||||
pos6 = None
|
||||
|
||||
users7 = users.order_by(User.coins_spent.desc()).limit(25).all()
|
||||
sq = g.db.query(User.id, func.rank().over(order_by=User.coins_spent.desc()).label("rank")).subquery()
|
||||
pos7 = g.db.query(sq.c.id, sq.c.rank).filter(sq.c.id == v.id).limit(1).one()[1]
|
||||
|
||||
try:
|
||||
pos9 = [x[0].id for x in users9].index(v.id)
|
||||
pos9 = (pos9+1, users9[pos9][1])
|
||||
except: pos9 = (len(users9)+1, 0)
|
||||
|
||||
users10 = users.order_by(User.truecoins.desc()).limit(25).all()
|
||||
sq = g.db.query(User.id, func.rank().over(order_by=User.truecoins.desc()).label("rank")).subquery()
|
||||
pos10 = g.db.query(sq.c.id, sq.c.rank).filter(sq.c.id == v.id).limit(1).one()[1]
|
||||
|
||||
sq = g.db.query(Badge.user_id, func.count(Badge.user_id).label("count"), func.rank().over(order_by=func.count(Badge.user_id).desc()).label("rank")).group_by(Badge.user_id).subquery()
|
||||
users11 = g.db.query(User, sq.c.count).join(sq, User.id==sq.c.user_id).order_by(sq.c.count.desc())
|
||||
pos11 = g.db.query(User.id, sq.c.rank, sq.c.count).join(sq, User.id==sq.c.user_id).filter(User.id == v.id).one_or_none()
|
||||
if pos11: pos11 = (pos11[1],pos11[2])
|
||||
else: pos11 = (users11.count()+1, 0)
|
||||
users11 = users11.limit(25).all()
|
||||
|
||||
if pos11[1] < 25 and v not in (x[0] for x in users11):
|
||||
pos11 = (26, pos11[1])
|
||||
|
||||
users12 = None
|
||||
pos12 = None
|
||||
|
||||
try:
|
||||
pos13 = [x[0].id for x in users13].index(v.id)
|
||||
pos13 = (pos13+1, users13[pos13][1])
|
||||
except: pos13 = (len(users13)+1, 0)
|
||||
|
||||
users14 = users.order_by(User.winnings.desc()).limit(25).all()
|
||||
sq = g.db.query(User.id, func.rank().over(order_by=User.winnings.desc()).label("rank")).subquery()
|
||||
pos14 = g.db.query(sq.c.id, sq.c.rank).filter(sq.c.id == v.id).limit(1).one()[1]
|
||||
|
||||
users15 = users.order_by(User.winnings).limit(25).all()
|
||||
sq = g.db.query(User.id, func.rank().over(order_by=User.winnings).label("rank")).subquery()
|
||||
pos15 = g.db.query(sq.c.id, sq.c.rank).filter(sq.c.id == v.id).limit(1).one()[1]
|
||||
|
||||
return render_template("leaderboard.html", v=v, users1=users1, pos1=pos1, users2=users2, pos2=pos2, users3=users3, pos3=pos3, users4=users4, pos4=pos4, users5=users5, pos5=pos5, users6=users6, pos6=pos6, users7=users7, pos7=pos7, users9=users9_25, pos9=pos9, users10=users10, pos10=pos10, users11=users11, pos11=pos11, users12=users12, pos12=pos12, users13=users13_25, pos13=pos13, users14=users14, pos14=pos14, users15=users15, pos15=pos15)
|
||||
return render_template("leaderboard.html", v=v, leaderboards=leaderboards)
|
||||
|
||||
@app.get("/@<username>/css")
|
||||
def get_css(username):
|
||||
|
@ -495,20 +396,6 @@ def get_profilecss(username):
|
|||
resp.headers.add("Content-Type", "text/css")
|
||||
return resp
|
||||
|
||||
@app.get("/@<username>/song")
|
||||
def usersong(username):
|
||||
user = get_user(username)
|
||||
if user.song: return redirect(f"/song/{user.song}.mp3")
|
||||
else: abort(404)
|
||||
|
||||
@app.get("/song/<song>")
|
||||
@app.get("/static/song/<song>")
|
||||
def song(song):
|
||||
resp = make_response(send_from_directory('/songs', song))
|
||||
resp.headers.remove("Cache-Control")
|
||||
resp.headers.add("Cache-Control", "public, max-age=3153600")
|
||||
return resp
|
||||
|
||||
@app.post("/subscribe/<post_id>")
|
||||
@limiter.limit("1/second;30/minute;200/hour;1000/day")
|
||||
@auth_required
|
||||
|
@ -529,8 +416,7 @@ def unsubscribe(v, post_id):
|
|||
return {"message": "Post unsubscribed!"}
|
||||
|
||||
@app.get("/report_bugs")
|
||||
@auth_required
|
||||
def reportbugs(v):
|
||||
def reportbugs():
|
||||
return redirect(f'/post/{BUG_THREAD}')
|
||||
|
||||
@app.post("/@<username>/message")
|
||||
|
@ -542,6 +428,10 @@ def message2(v, username):
|
|||
"contact modmail if you think this decision was incorrect.")
|
||||
|
||||
user = get_user(username, v=v, include_blocks=True)
|
||||
|
||||
if user.id == MODMAIL_ID:
|
||||
abort(403, "Please use modmail to contact the admins")
|
||||
|
||||
if hasattr(user, 'is_blocking') and user.is_blocking: abort(403, "You're blocking this user.")
|
||||
|
||||
if v.admin_level <= 1 and hasattr(user, 'is_blocked') and user.is_blocked:
|
||||
|
@ -550,7 +440,6 @@ def message2(v, username):
|
|||
message = request.values.get("message", "").strip()[:10000].strip()
|
||||
|
||||
if not message: abort(400, "Message is empty!")
|
||||
|
||||
body_html = sanitize(message)
|
||||
|
||||
existing = g.db.query(Comment.id).filter(Comment.author_id == v.id,
|
||||
|
@ -567,7 +456,6 @@ def message2(v, username):
|
|||
body_html=body_html
|
||||
)
|
||||
g.db.add(c)
|
||||
|
||||
g.db.flush()
|
||||
|
||||
c.top_comment_id = c.id
|
||||
|
@ -602,31 +490,19 @@ def messagereply(v):
|
|||
parent = get_comment(id, v=v)
|
||||
user_id = parent.author.id
|
||||
|
||||
if parent.sentto == 2: user_id = None
|
||||
if parent.sentto == MODMAIL_ID: user_id = None
|
||||
elif v.id == user_id: user_id = parent.sentto
|
||||
|
||||
body_html = sanitize(message)
|
||||
|
||||
if parent.sentto == 2 and request.files.get("file") and request.headers.get("cf-ipcountry") != "T1":
|
||||
if parent.sentto == MODMAIL_ID and request.files.get("file") and request.headers.get("cf-ipcountry") != "T1":
|
||||
file=request.files["file"]
|
||||
if file.content_type.startswith('image/'):
|
||||
name = f'/images/{time.time()}'.replace('.','') + '.webp'
|
||||
file.save(name)
|
||||
url = process_image(name)
|
||||
body_html += f'<img data-bs-target="#expandImageModal" data-bs-toggle="modal" onclick="expandDesktopImage(this.src)" class="img" src="{url}" loading="lazy">'
|
||||
elif file.content_type.startswith('video/'):
|
||||
file.save("video.mp4")
|
||||
with open("video.mp4", 'rb') as f:
|
||||
try: req = requests.request("POST", "https://api.imgur.com/3/upload", headers={'Authorization': f'Client-ID {IMGUR_KEY}'}, files=[('video', f)], timeout=5).json()['data']
|
||||
except requests.Timeout: abort(500, "Video upload timed out, please try again!")
|
||||
try: url = req['link']
|
||||
except:
|
||||
error = req['error']
|
||||
if error == 'File exceeds max duration': error += ' (60 seconds)'
|
||||
abort(400, error)
|
||||
if url.endswith('.'): url += 'mp4'
|
||||
body_html += f"<p>{url}</p>"
|
||||
else: abort(400, "Image/Video files only")
|
||||
else: abort(400, "Image files only")
|
||||
|
||||
|
||||
c = Comment(author_id=v.id,
|
||||
|
@ -678,7 +554,7 @@ def messagereply(v):
|
|||
)
|
||||
|
||||
|
||||
if c.top_comment.sentto == 2:
|
||||
if c.top_comment.sentto == MODMAIL_ID:
|
||||
admins = g.db.query(User).filter(User.admin_level > 2, User.id != v.id).all()
|
||||
for admin in admins:
|
||||
notif = Notification(comment_id=c.id, user_id=admin.id)
|
||||
|
@ -737,15 +613,13 @@ def api_is_available(name):
|
|||
else:
|
||||
return {name: True}
|
||||
|
||||
@app.get("/id/<id>")
|
||||
@auth_desired
|
||||
def user_id(v, id):
|
||||
@app.get("/id/<int:id>")
|
||||
def user_id(id:int):
|
||||
user = get_account(id)
|
||||
return redirect(user.url)
|
||||
|
||||
@app.get("/u/<username>")
|
||||
@auth_desired
|
||||
def redditor_moment_redirect(v, username):
|
||||
def redditor_moment_redirect(username:str):
|
||||
return redirect(f"/@{username}")
|
||||
|
||||
@app.get("/@<username>/followers")
|
||||
|
@ -1002,7 +876,6 @@ import re
|
|||
@app.get("/uid/<id>/pic")
|
||||
@app.get("/uid/<id>/pic/profile")
|
||||
@limiter.exempt
|
||||
@auth_desired
|
||||
def user_profile_uid(v, id):
|
||||
try: id = int(id)
|
||||
except:
|
||||
|
@ -1033,8 +906,7 @@ def user_profile_uid(v, id):
|
|||
|
||||
@app.get("/@<username>/pic")
|
||||
@limiter.exempt
|
||||
@auth_required
|
||||
def user_profile_name(v, username):
|
||||
def user_profile_name(username:str):
|
||||
|
||||
name = f"/@{username}/pic"
|
||||
path = cache.get(name)
|
||||
|
|
|
@ -21,8 +21,6 @@ def admin_vote_info_get(v):
|
|||
|
||||
if thing.ghost and v.id != OWNER_ID: abort(403)
|
||||
|
||||
if not thing.author:
|
||||
print(thing.id, flush=True)
|
||||
if isinstance(thing, Submission):
|
||||
if thing.author.shadowbanned and not (v and v.admin_level):
|
||||
thing_id = g.db.query(Submission.id).filter_by(upvotes=thing.upvotes, downvotes=thing.downvotes).order_by(Submission.id).first()[0]
|
||||
|
|
1
files/templates/admin/shadowbanned_tooltip.html
Normal file
1
files/templates/admin/shadowbanned_tooltip.html
Normal file
|
@ -0,0 +1 @@
|
|||
{% if v and v.admin_level >= PERMS['USER_SHADOWBAN'] and user.shadowbanned %}<i class="fas fa-user-times text-admin" data-bs-toggle="tooltip" data-bs-placement="bottom" title='Shadowbanned by @{{user.shadowbanner}}{% if user.ban_reason %} for "{{user.ban_reason}}"{% endif %}'></i>{% endif %}
|
|
@ -44,4 +44,6 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
{% if FEATURES['AWARDS'] %}
|
||||
<script src="{{ 'js/award_modal.js' | asset }}" data-cfasync="false"></script>
|
||||
{% endif %}
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
{% extends "settings2.html" %}
|
||||
{%- import 'component/sorting_time.html' as sorting_time -%}
|
||||
|
||||
{% block pagetitle %}Changelog{% endblock %}
|
||||
|
||||
|
@ -35,27 +36,7 @@
|
|||
</div>
|
||||
|
||||
<div class="text-small font-weight-bold ml-3 mr-2"></div>
|
||||
<div class="dropdown dropdown-actions">
|
||||
<button class="btn btn-secondary dropdown-toggle" role="button" id="dropdownMenuButton2" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
{% if sort=="hot" %}<i class="fas fa-fire mr-1"></i>{% endif %}
|
||||
{% if sort=="top" %}<i class="fas fa-arrow-alt-circle-up mr-1"></i>{% endif %}
|
||||
{% if sort=="bottom" %}<i class="fas fa-arrow-alt-circle-down mr-1"></i>{% endif %}
|
||||
{% if sort=="new" %}<i class="fas fa-sparkles mr-1"></i>{% endif %}
|
||||
{% if sort=="old" %}<i class="fas fa-book mr-1"></i>{% endif %}
|
||||
{% if sort=="controversial" %}<i class="fas fa-bullhorn mr-1"></i>{% endif %}
|
||||
{% if sort=="comments" %}<i class="fas fa-comments mr-1"></i>{% endif %}
|
||||
{{sort | capitalize}}
|
||||
</button>
|
||||
<div class="dropdown-menu" aria-labelledby="dropdownMenuButton2" x-placement="bottom-start" style="position: absolute; will-change: transform; top: 0px; left: 0px; transform: translate3d(0px, 31px, 0px);">
|
||||
{% if sort != "hot" %}<a class="dropdown-item" href="?sort=hot&t={{t}}"><i class="fas fa-fire mr-2"></i>Hot</a>{% endif %}
|
||||
{% if sort != "top" %}<a class="dropdown-item" href="?sort=top&t={{t}}"><i class="fas fa-arrow-alt-circle-up mr-2"></i>Top</a>{% endif %}
|
||||
{% if sort != "bottom" %}<a class="dropdown-item" href="?sort=bottom&t={{t}}"><i class="fas fa-arrow-alt-circle-down mr-2"></i>Bottom</a>{% endif %}
|
||||
{% if sort != "new" %}<a class="dropdown-item" href="?sort=new&t={{t}}"><i class="fas fa-sparkles mr-2"></i>New</a>{% endif %}
|
||||
{% if sort != "old" %}<a class="dropdown-item" href="?sort=old&t={{t}}"><i class="fas fa-book mr-2"></i>Old</a>{% endif %}
|
||||
{% if sort != "controversial" %}<a class="dropdown-item" href="?sort=controversial&t={{t}}"><i class="fas fa-bullhorn mr-2"></i>Controversial</a>{% endif %}
|
||||
{% if sort != "comments" %}<a class="dropdown-item" href="?sort=comments&t={{t}}"><i class="fas fa-comments mr-2"></i>Comments</a>{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{{sorting_time.sort_dropdown(sort, t, SORTS_POSTS)}}
|
||||
</div>
|
||||
{% endblock %}
|
||||
</div>
|
||||
|
@ -72,13 +53,9 @@
|
|||
{% endif %}
|
||||
|
||||
<div class="row no-gutters {% if listing %}mt-md-3{% elif not listing %}my-md-3{% endif %}">
|
||||
|
||||
<div class="col-12">
|
||||
|
||||
<div class="posts" id="posts">
|
||||
|
||||
{% include "submission_listing.html" %}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -47,7 +47,7 @@
|
|||
{% macro single_comment(c, level) %}
|
||||
|
||||
|
||||
{% if c.should_hide_score %}
|
||||
{% if should_hide_score or c.should_hide_score %}
|
||||
{% set ups="" %}
|
||||
{% set downs="" %}
|
||||
{% set score="" %}
|
||||
|
@ -72,20 +72,14 @@
|
|||
</div>
|
||||
|
||||
<div class="comment-user-info">
|
||||
{% if standalone and c.over_18 %}<span class="badge badge-danger">+18</span> {% endif %}
|
||||
{% if standalone and c.over_18 %}<span class="badge badge-danger">+18</span>{% endif %}
|
||||
{% if c.is_banned %}removed by @{{c.ban_reason}}{% elif c.deleted_utc %}Deleted by author{% elif c.is_blocking %}You are blocking @{{c.author_name}}{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="comment-body">
|
||||
|
||||
<div id="comment-{{c.id}}-only" class="{% if c.award_count('glowie') %}glow{% endif %} comment-{{c.id}}-only">
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
<div id="comment-{{c.id}}-only" class="comment-{{c.id}}-only"></div>
|
||||
{% if render_replies %}
|
||||
{% if level<9 %}
|
||||
{% if level <= RENDER_DEPTH_LIMIT - 1 %}
|
||||
<div id="replies-of-{{c.id}}" class="">
|
||||
{% set standalone=False %}
|
||||
{% for reply in replies %}
|
||||
|
@ -143,7 +137,7 @@
|
|||
{% elif c.author_id==NOTIFICATIONS_ID or c.author_id==AUTOJANNY_ID %}
|
||||
<span class="font-weight-bold">Notification</span>
|
||||
{% else %}
|
||||
{% if c.sentto == 2 %}
|
||||
{% if c.sentto == MODMAIL_ID %}
|
||||
<span class="font-weight-bold">Sent to admins</span>
|
||||
{% else %}
|
||||
<span class="font-weight-bold">Sent to @{{c.senttouser.username}}</span>
|
||||
|
@ -153,7 +147,7 @@
|
|||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if c.parent_comment and c.parent_comment.sentto %}
|
||||
{% if not standalone and c.parent_comment and c.parent_comment.sentto %}
|
||||
{% set isreply = True %}
|
||||
{% else %}
|
||||
{% set isreply = False %}
|
||||
|
@ -171,25 +165,27 @@
|
|||
{% if c.ghost %}
|
||||
👻
|
||||
{% else %}
|
||||
|
||||
{% if c.author.verified %}<i class="fas fa-badge-check align-middle ml-1 {% if c.author.verified=='Glowiefied' %}glow{% endif %}" style="color:{% if c.author.verifiedcolor %}#{{c.author.verifiedcolor}}{% else %}#1DA1F2{% endif %}" data-bs-toggle="tooltip" data-bs-placement="bottom" title="{{c.author.verified}}"></i>
|
||||
{% endif %}
|
||||
{% if not c.author %}
|
||||
{{c.print()}}
|
||||
{% if not should_hide_username %}
|
||||
<a href="/@{{c.author_name}}" class="user-name" onclick='popclick({{c.author.json_popover(v) | tojson}}); return false' data-bs-placement="bottom" data-bs-toggle="popover" data-bs-trigger="click" data-content-id="popover" role="button" tabindex="0" style="font-size:12px; font-weight:bold;"><img loading="lazy" src="{{c.author.profile_url}}" class="profile-pic-20 mr-2"><span {% if c.author.patron and not c.distinguish_level %}class="patron" style="background-color:#{{c.author.namecolor}};"{% elif c.distinguish_level %}class="mod"{% endif %}>{{c.author_name}}</span></a>
|
||||
{% endif %}
|
||||
<a href="/@{{c.author_name}}" class="user-name" onclick='popclick({{c.author.json_popover(v) | tojson}}); return false' data-bs-placement="bottom" data-bs-toggle="popover" data-bs-trigger="click" data-content-id="popover" role="button" tabindex="0" style="font-size:12px; font-weight:bold;"><img loading="lazy" src="{{c.author.profile_url}}" class="profile-pic-20 mr-2"><span {% if c.author.patron and not c.distinguish_level %}class="patron" style="background-color:#{{c.author.namecolor}};"{% elif c.distinguish_level %}class="mod"{% endif %}>{{c.author_name}}</span></a>
|
||||
{% if v and v.admin_level > 1 %}
|
||||
<span
|
||||
class="usernote-link"
|
||||
data-micromodal-trigger="modal-1"
|
||||
onclick='fillnote( {{c.author.json_notes(v) | tojson}}, null, {{c.id}} )'>U</span>
|
||||
{% endif %}
|
||||
{% if c.author.customtitle %} <bdi style="color: #{{c.author.titlecolor}}"> {{c.author.customtitle | safe}}</bdi>{% endif %}
|
||||
{% if c.author.customtitle and not should_hide_username -%}
|
||||
<bdi style="color: #{{c.author.titlecolor}}"> {{c.author.customtitle | safe}}</bdi>
|
||||
{%- endif %}
|
||||
{% endif %}
|
||||
|
||||
{% for a in c.awards|reverse %}
|
||||
<i class="{{a.class_list}} px-1" data-bs-toggle="tooltip" data-bs-placement="bottom" title="{{a.title}} Award given by @{{a.user.username}}"></i>
|
||||
{% endfor %}
|
||||
{% if FEATURES['AWARDS'] %}
|
||||
{% for a in c.awards|reverse %}
|
||||
<i class="{{a.class_list}} px-1" data-bs-toggle="tooltip" data-bs-placement="bottom" title="{{a.title}} Award given by @{{a.user.username}}"></i>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
|
||||
{% if c.bannedfor %}
|
||||
<a role="button"><i class="fas fa-hammer-crash text-danger" data-bs-toggle="tooltip" data-bs-placement="bottom" title="User was banned for this comment{% if c.author.banned_by %} by @{{c.author.banned_by.username}}{% endif %}"></i></a>
|
||||
|
@ -221,7 +217,7 @@
|
|||
|
||||
<div class="comment-body">
|
||||
|
||||
<div id="{% if comment_info and comment_info.id == c.id %}context{%else%}comment-{{c.id}}-only{% endif %}" class="{% if c.unread %}unread{% endif %} {% if c.award_count('glowie') %}glow{% endif %} comment-{{c.id}}-only comment-anchor {% if comment_info and comment_info.id == c.id %}context{%endif%}{% if c.is_banned %} banned{% endif %}{% if c.deleted_utc %} deleted{% endif %}">
|
||||
<div id="{% if comment_info and comment_info.id == c.id %}context{%else%}comment-{{c.id}}-only{% endif %}" class="{% if c.unread %}unread{% endif %} comment-{{c.id}}-only comment-anchor {% if comment_info and comment_info.id == c.id %}context{%endif%}{% if c.is_banned %} banned{% endif %}{% if c.deleted_utc %} deleted{% endif %}">
|
||||
|
||||
|
||||
{% if v and c.filter_state == 'reported' and v.can_manage_reports() %}
|
||||
|
@ -487,7 +483,7 @@
|
|||
|
||||
<label class="btn btn-secondary format m-0" for="file-upload-reply-{{c.fullname}}">
|
||||
<div id="filename-show-reply-{{c.fullname}}"><i class="far fa-image"></i></div>
|
||||
<input autocomplete="off" id="file-upload-reply-{{c.fullname}}" type="file" multiple="multiple" name="file" accept="image/*, video/*" {% if request.headers.get('cf-ipcountry')=="T1" %}disabled{% endif %} onchange="changename('filename-show-reply-{{c.fullname}}','file-upload-reply-{{c.fullname}}')" hidden>
|
||||
<input autocomplete="off" id="file-upload-reply-{{c.fullname}}" type="file" multiple="multiple" name="file" accept="image/*" {% if request.headers.get('cf-ipcountry')=="T1" %}disabled{% endif %} onchange="changename('filename-show-reply-{{c.fullname}}','file-upload-reply-{{c.fullname}}')" hidden>
|
||||
</label>
|
||||
</div>
|
||||
<a id="save-reply-to-{{c.fullname}}" class="btn btn-primary ml-2 fl-r commentmob" onclick="post_comment('{{c.fullname}}', '{{c.post.id}}', {{level}})"role="button">Comment</a>
|
||||
|
@ -500,7 +496,7 @@
|
|||
|
||||
|
||||
{% if render_replies %}
|
||||
{% if level<9 or request.path == '/notifications' %}
|
||||
{% if level <= RENDER_DEPTH_LIMIT - 1 or request.path == '/notifications' %}
|
||||
<div id="replies-of-{{c.id}}">
|
||||
{% for reply in replies %}
|
||||
{{single_comment(reply, level=level+1)}}
|
||||
|
@ -522,10 +518,10 @@
|
|||
<textarea required autocomplete="off" minlength="1" maxlength="10000" name="body" form="reply-to-t3_{{c.id}}" data-id="{{c.id}}" class="comment-box form-control rounded" id="reply-form-body-{{c.id}}" aria-label="With textarea" rows="3" oninput="markdown('reply-form-body-{{c.id}}', 'message-reply-{{c.id}}')"></textarea>
|
||||
<div class="comment-format" id="comment-format-bar-{{c.id}}">
|
||||
|
||||
{% if c.sentto == 2 %}
|
||||
{% if c.sentto == MODMAIL_ID %}
|
||||
<label class="btn btn-secondary m-0 mt-3" for="file-upload">
|
||||
<div id="filename"><i class="far fa-image"></i></div>
|
||||
<input autocomplete="off" id="file-upload" type="file" name="file" accept="image/*, video/*" {% if request.headers.get('cf-ipcountry')=="T1" %}disabled{% endif %} onchange="changename('filename','file-upload')" hidden>
|
||||
<input autocomplete="off" id="file-upload" type="file" name="file" accept="image/*" {% if request.headers.get('cf-ipcountry')=="T1" %}disabled{% endif %} onchange="changename('filename','file-upload')" hidden>
|
||||
</label>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
@ -748,7 +744,9 @@
|
|||
<script src="{{ 'js/vendor/marked.min.js' | asset }}"></script>
|
||||
<script src="{{ 'js/marked.custom.js' | asset }}"></script>
|
||||
<script src="{{ 'js/comments_v.js' | asset }}"></script>
|
||||
<script src="{{ 'js/award_modal.js' | asset }}"></script>
|
||||
{% if FEATURES['AWARDS'] %}
|
||||
<script src="{{ 'js/award_modal.js' | asset }}"></script>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
<script src="{{ 'js/clipboard.js' | asset }}"></script>
|
||||
|
|
14
files/templates/component/sorting_time.html
Normal file
14
files/templates/component/sorting_time.html
Normal file
|
@ -0,0 +1,14 @@
|
|||
{%- macro sort_dropdown(sort, t, all_sorts, extra_query='') -%}
|
||||
<div class="dropdown dropdown-actions">
|
||||
<button class="btn btn-secondary dropdown-toggle" role="button" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
<i class="fas {{all_sorts[sort]}} mr-1"></i> {{sort|capitalize}}
|
||||
</button>
|
||||
<div class="dropdown-menu" x-placement="bottom-start" style="position: absolute; will-change: transform; top: 0px; left: 0px; transform: translateY(31px);">
|
||||
{% for possible_sort in all_sorts %}
|
||||
{%- if sort != possible_sort -%}
|
||||
<a class="dropdown-item" href="?{{extra_query ~ '&' if extra_query else ''}}sort={{possible_sort}}{% if t %}&t={{t}}{% endif %}"><i class="fas {{all_sorts[possible_sort]}} mr-1"></i> {{possible_sort | capitalize}}</a>
|
||||
{%- endif -%}
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{%- endmacro -%}
|
|
@ -1,12 +1,8 @@
|
|||
{% extends "default.html" %}
|
||||
|
||||
{% block title %}
|
||||
<title>{{SITE_TITLE}} - Contact</title>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
{% if msg %}
|
||||
<div class="alert alert-success alert-dismissible fade show my-3" role="alert">
|
||||
<i class="fas fa-check-circle my-auto" aria-hidden="true"></i>
|
||||
|
@ -18,7 +14,7 @@
|
|||
</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<section id="contact">
|
||||
<h1 class="article-title">Contact {{SITE_TITLE}} Admins</h1>
|
||||
|
||||
<p>Use this form to contact {{SITE_TITLE}} Admins.</p>
|
||||
|
@ -32,20 +28,18 @@
|
|||
<textarea autocomplete="off" maxlength="10000" id="input-message" form="contactform" name="message" class="form-control" required></textarea>
|
||||
<label class="btn btn-secondary m-0 mt-3" for="file-upload">
|
||||
<div id="filename"><i class="far fa-image"></i></div>
|
||||
<input autocomplete="off" id="file-upload" type="file" name="file" accept="image/*, video/*" {% if request.headers.get('cf-ipcountry')=="T1" %}disabled{% endif %} onchange="changename('filename','file-upload')" hidden>
|
||||
<input autocomplete="off" id="file-upload" type="file" name="file" accept="image/*" {% if request.headers.get('cf-ipcountry')=="T1" %}disabled{% endif %} onchange="changename('filename','file-upload')" hidden>
|
||||
</label>
|
||||
{% if not v and hcaptcha %}
|
||||
<div class="h-captcha" data-sitekey="{{hcaptcha}}"></div>
|
||||
{% endif %}
|
||||
<input type="submit" value="Submit" class="btn btn-primary mt-3">
|
||||
</form>
|
||||
|
||||
<pre>
|
||||
|
||||
|
||||
</pre>
|
||||
|
||||
<p>If you can see this line, we haven't been contacted by any law enforcement or governmental organizations in 2022 yet.</p>
|
||||
|
||||
<pre>
|
||||
|
||||
|
||||
</pre>
|
||||
</section>
|
||||
<section id="canary">
|
||||
<p>If you can see this line, we haven't been contacted by any law enforcement or governmental organizations in 2022 yet.</p>
|
||||
</section>
|
||||
{% if hcaptcha %}
|
||||
<script src="{{ 'js/hcaptcha.js' | asset }}"></script>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
{% extends "default.html" %}
|
||||
|
||||
{%- import 'component/sorting_time.html' as sorting_time -%}
|
||||
{% block desktopBanner %}
|
||||
|
||||
{% if v and environ.get("FP") %}
|
||||
|
@ -73,30 +73,8 @@
|
|||
{% if t != "all" %}<a class="dropdown-item" href="?sort={{sort}}&t=all&ccmode={{ccmode}}"><i class="fas fa-infinity mr-2 "></i>All</a>{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="dropdown dropdown-actions ml-2">
|
||||
<button class="btn btn-secondary dropdown-toggle" type="button" id="dropdownMenuButton2" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
{% if sort=="hot" %}<i class="fas fa-fire mr-2 "></i>{% endif %}
|
||||
{% if sort=="bump" %}<i class="fas fa-arrow-up mr-2 "></i>{% endif %}
|
||||
{% if sort=="top" %}<i class="fas fa-arrow-alt-circle-up mr-2 "></i>{% endif %}
|
||||
{% if sort=="bottom" %}<i class="fas fa-arrow-alt-circle-down mr-2 "></i>{% endif %}
|
||||
{% if sort=="new" %}<i class="fas fa-sparkles mr-2 "></i>{% endif %}
|
||||
{% if sort=="old" %}<i class="fas fa-book mr-2 "></i>{% endif %}
|
||||
{% if sort=="controversial" %}<i class="fas fa-bullhorn mr-2 "></i>{% endif %}
|
||||
{% if sort=="comments" %}<i class="fas fa-comments mr-2 "></i>{% endif %}
|
||||
{{sort | capitalize}}
|
||||
</button>
|
||||
<div class="dropdown-menu" aria-labelledby="dropdownMenuButton2" x-placement="bottom-start" style="position: absolute; will-change: transform; top: 0px; left: 0px; transform: translate3d(0px, 31px, 0px);">
|
||||
{% if sort != "hot" %}<a class="dropdown-item" href="?sort=hot&t={{t}}&ccmode={{ccmode}}"><i class="fas fa-fire mr-2 "></i>Hot</a>{% endif %}
|
||||
{% if sort != "bump" %}<a class="dropdown-item" href="?sort=bump&t={{t}}&ccmode={{ccmode}}"><i class="fas fa-arrow-up mr-2 "></i>Bump</a>{% endif %}
|
||||
{% if sort != "top" %}<a class="dropdown-item" href="?sort=top&t={{t}}&ccmode={{ccmode}}"><i class="fas fa-arrow-alt-circle-up mr-2 "></i>Top</a>{% endif %}
|
||||
{% if sort != "bottom" %}<a class="dropdown-item" href="?sort=bottom&t={{t}}&ccmode={{ccmode}}"><i class="fas fa-arrow-alt-circle-down mr-2 "></i>Bottom</a>{% endif %}
|
||||
{% if sort != "new" %}<a class="dropdown-item" href="?sort=new&t={{t}}&ccmode={{ccmode}}"><i class="fas fa-sparkles mr-2 "></i>New</a>{% endif %}
|
||||
{% if sort != "old" %}<a class="dropdown-item" href="?sort=old&t={{t}}&ccmode={{ccmode}}"><i class="fas fa-book mr-2 "></i>Old</a>{% endif %}
|
||||
{% if sort != "controversial" %}<a class="dropdown-item" href="?sort=controversial&t={{t}}&ccmode={{ccmode}}"><i class="fas fa-bullhorn mr-2 "></i>Controversial</a>{% endif %}
|
||||
{% if sort != "comments" %}<a class="dropdown-item" href="?sort=comments&t={{t}}&ccmode={{ccmode}}"><i class="fas fa-comments mr-2 "></i>Comments</a>{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% set ccmode_text = 'ccmode=' ~ ccmode %}
|
||||
{{sorting_time.sort_dropdown(sort, t, SORTS_POSTS, ccmode_text)}}
|
||||
</div>
|
||||
{% endblock %}
|
||||
</div>
|
||||
|
@ -104,21 +82,14 @@
|
|||
</div>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<div class="row no-gutters {% if listing %}mt-md-3{% elif not listing %}my-md-3{% endif %}">
|
||||
|
||||
<div class="col-12">
|
||||
|
||||
<div class="posts" id="posts">
|
||||
|
||||
{% include "submission_listing.html" %}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block pagenav %}
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
{% extends "default.html" %}
|
||||
{%- import 'component/sorting_time.html' as sorting_time -%}
|
||||
|
||||
{% block sortnav %}{% endblock %}
|
||||
|
||||
|
@ -30,39 +31,17 @@
|
|||
</div>
|
||||
|
||||
<div class="text-small font-weight-bold ml-3 mr-2"></div>
|
||||
<div class="dropdown dropdown-actions">
|
||||
<button class="btn btn-secondary dropdown-toggle" role="button" id="dropdownMenuButton2" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
{% if sort=="top" %}<i class="fas fa-arrow-alt-circle-up mr-1"></i>{% endif %}
|
||||
{% if sort=="bottom" %}<i class="fas fa-arrow-alt-circle-down mr-1"></i>{% endif %}
|
||||
{% if sort=="new" %}<i class="fas fa-sparkles mr-1"></i>{% endif %}
|
||||
{% if sort=="old" %}<i class="fas fa-book mr-1"></i>{% endif %}
|
||||
{% if sort=="controversial" %}<i class="fas fa-bullhorn mr-1"></i>{% endif %}
|
||||
{{sort | capitalize}}
|
||||
</button>
|
||||
<div class="dropdown-menu" aria-labelledby="dropdownMenuButton2" x-placement="bottom-start" style="position: absolute; will-change: transform; top: 0px; left: 0px; transform: translate3d(0px, 31px, 0px);">
|
||||
{% if sort != "top" %}<a class="dropdown-item" href="?sort=top&t={{t}}"><i class="fas fa-arrow-alt-circle-up mr-2"></i>Top</a>{% endif %}
|
||||
{% if sort != "bottom" %}<a class="dropdown-item" href="?sort=bottom&t={{t}}"><i class="fas fa-arrow-alt-circle-down mr-2"></i>Bottom</a>{% endif %}
|
||||
{% if sort != "new" %}<a class="dropdown-item" href="?sort=new&t={{t}}"><i class="fas fa-sparkles mr-2"></i>New</a>{% endif %}
|
||||
{% if sort != "old" %}<a class="dropdown-item" href="?sort=old&t={{t}}"><i class="fas fa-book mr-2"></i>Old</a>{% endif %}
|
||||
{% if sort != "controversial" %}<a class="dropdown-item" href="?sort=controversial&t={{t}}"><i class="fas fa-bullhorn mr-2"></i>Controversial</a>{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{{sorting_time.sort_dropdown(sort, t, SORTS_COMMENTS)}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row no-gutters {% if listing %}mt-md-3{% elif not listing %}my-md-3{% endif %}">
|
||||
|
||||
<div class="col-12 px-3">
|
||||
|
||||
<div class="posts" id="posts">
|
||||
|
||||
{% include "comments.html" %}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block pagenav %}
|
||||
|
|
|
@ -1,484 +1,62 @@
|
|||
{% extends "settings2.html" %}
|
||||
|
||||
{% block pagetitle %}Leaderboard{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<pre class="d-none d-md-inline-block"></pre>
|
||||
<h5 style="font-weight:bold;text-align: center">Top 25 by coins</h5>
|
||||
<pre></pre>
|
||||
<div class="overflow-x-auto"><table class="table table-striped mb-5">
|
||||
<thead class="bg-primary text-white">
|
||||
<tr>
|
||||
<th>#</th>
|
||||
<th>Name</th>
|
||||
<th>Coins</th>
|
||||
</tr>
|
||||
</thead>
|
||||
{% for user in users1 %}
|
||||
<tr {% if v.id == user.id %}class="self"{% endif %}>
|
||||
<td>{{loop.index}}</td>
|
||||
<td><a style="color:#{{user.namecolor}};font-weight:bold" href="/@{{user.username}}"><img loading="lazy" src="{{user.profile_url}}" class="pp20"><span {% if user.patron %}class="patron" style="background-color:#{{user.namecolor}}"{% endif %}>{{user.username}}</span></a></td>
|
||||
<td>{{user.coins}}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% if pos1 > 25 %}
|
||||
<tr style="border-top:2px solid var(--primary)">
|
||||
<td>{{pos1}}</td>
|
||||
<td><a style="color:#{{v.namecolor}};font-weight:bold" href="/@{{v.username}}"><img loading="lazy" src="{{v.profile_url}}" class="pp20"><span {% if v.patron %}class="patron" style="background-color:#{{v.namecolor}}"{% endif %}>{{v.username}}</span></a></td>
|
||||
<td>{{v.coins}}</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
</table>
|
||||
|
||||
|
||||
<pre>
|
||||
|
||||
|
||||
</pre>
|
||||
<h5 style="font-weight:bold;text-align: center">Top 25 by coins spent in shop</h5>
|
||||
<pre>
|
||||
|
||||
|
||||
</pre>
|
||||
<div class="overflow-x-auto"><table class="table table-striped mb-5">
|
||||
<thead class="bg-primary text-white">
|
||||
<tr>
|
||||
<th>#</th>
|
||||
<th>Name</th>
|
||||
<th>Coins</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
{% for user in users7 %}
|
||||
<tr {% if v.id == user.id %}class="self"{% endif %}>
|
||||
<td>{{loop.index}}</td>
|
||||
<td><a style="color:#{{user.namecolor}};font-weight:bold" href="/@{{user.username}}"><img loading="lazy" src="{{user.profile_url}}" class="pp20"><span {% if user.patron %}class="patron" style="background-color:#{{user.namecolor}}"{% endif %}>{{user.username}}</span></a></td>
|
||||
<td>{{user.coins_spent}}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% if pos7 > 25 %}
|
||||
<tr style="border-top:2px solid var(--primary)">
|
||||
<td>{{pos7}}</td>
|
||||
<td><a style="color:#{{v.namecolor}};font-weight:bold" href="/@{{v.username}}"><img loading="lazy" src="{{v.profile_url}}" class="pp20"><span {% if v.patron %}class="patron" style="background-color:#{{v.namecolor}}"{% endif %}>{{v.username}}</span></a></td>
|
||||
<td>{{v.coins_spent}}</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
</table>
|
||||
|
||||
|
||||
<pre>
|
||||
|
||||
|
||||
</pre>
|
||||
<h5 style="font-weight:bold;text-align: center">Top 25 by truescore</h5>
|
||||
<pre>
|
||||
|
||||
|
||||
</pre>
|
||||
<div class="overflow-x-auto"><table class="table table-striped mb-5">
|
||||
<thead class="bg-primary text-white">
|
||||
<tr>
|
||||
<th>#</th>
|
||||
<th>Name</th>
|
||||
<th>Truescore</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="followers-table">
|
||||
{% for user in users10 %}
|
||||
<tr {% if v.id == user.id %}class="self"{% endif %}>
|
||||
<td>{{loop.index}}</td>
|
||||
<td><a style="color:#{{user.namecolor}};font-weight:bold" href="/@{{user.username}}"><img loading="lazy" src="{{user.profile_url}}" class="pp20"><span {% if user.patron %}class="patron" style="background-color:#{{user.namecolor}}"{% endif %}>{{user.username}}</span></a></td>
|
||||
<td>{{user.truecoins}}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% if pos10 > 25 %}
|
||||
<tr style="border-top:2px solid var(--primary)">
|
||||
<td>{{pos10}}</td>
|
||||
<td><a style="color:#{{v.namecolor}};font-weight:bold" href="/@{{v.username}}"><img loading="lazy" src="{{v.profile_url}}" class="pp20"><span {% if v.patron %}class="patron" style="background-color:#{{v.namecolor}}"{% endif %}>{{v.username}}</span></a></td>
|
||||
<td>{{v.truecoins}}</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<pre>
|
||||
|
||||
|
||||
</pre>
|
||||
<h5 style="font-weight:bold;text-align: center">Top 25 by followers</h5>
|
||||
<pre>
|
||||
|
||||
|
||||
</pre>
|
||||
<div class="overflow-x-auto"><table class="table table-striped mb-5">
|
||||
<thead class="bg-primary text-white">
|
||||
<tr>
|
||||
<th>#</th>
|
||||
<th>Name</th>
|
||||
<th>Followers</th>
|
||||
</tr>
|
||||
</thead>
|
||||
{% for user in users2 %}
|
||||
<tr {% if v.id == user.id %}class="self"{% endif %}>
|
||||
<td>{{loop.index}}</td>
|
||||
<td><a style="color:#{{user.namecolor}};font-weight:bold" href="/@{{user.username}}"><img loading="lazy" src="{{user.profile_url}}" class="pp20"><span {% if user.patron %}class="patron" style="background-color:#{{user.namecolor}}"{% endif %}>{{user.username}}</span></a></td>
|
||||
<td>{{user.stored_subscriber_count}}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% if pos2 > 25 %}
|
||||
<tr style="border-top:2px solid var(--primary)">
|
||||
<td>{{pos2}}</td>
|
||||
<td><a style="color:#{{v.namecolor}};font-weight:bold" href="/@{{v.username}}"><img loading="lazy" src="{{v.profile_url}}" class="pp20"><span {% if v.patron %}class="patron" style="background-color:#{{v.namecolor}}"{% endif %}>{{v.username}}</span></a></td>
|
||||
<td>{{v.stored_subscriber_count}}</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
</table>
|
||||
<pre>
|
||||
|
||||
|
||||
</pre>
|
||||
<h5 style="font-weight:bold;text-align: center">Top 25 by post count</h5>
|
||||
<pre>
|
||||
|
||||
|
||||
</pre>
|
||||
<div class="overflow-x-auto"><table class="table table-striped mb-5">
|
||||
<thead class="bg-primary text-white">
|
||||
<tr>
|
||||
<th>#</th>
|
||||
<th>Name</th>
|
||||
<th>Posts</th>
|
||||
</tr>
|
||||
</thead>
|
||||
{% for user in users3 %}
|
||||
<tr {% if v.id == user.id %}class="self"{% endif %}>
|
||||
<td>{{loop.index}}</td>
|
||||
<td><a style="color:#{{user.namecolor}};font-weight:bold" href="/@{{user.username}}"><img loading="lazy" src="{{user.profile_url}}" class="pp20"><span {% if user.patron %}class="patron" style="background-color:#{{user.namecolor}}"{% endif %}>{{user.username}}</span></a></td>
|
||||
<td>{{user.post_count}}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% if pos3 > 25 %}
|
||||
<tr style="border-top:2px solid var(--primary)">
|
||||
<td>{{pos3}}</td>
|
||||
<td><a style="color:#{{v.namecolor}};font-weight:bold" href="/@{{v.username}}"><img loading="lazy" src="{{v.profile_url}}" class="pp20"><span {% if v.patron %}class="patron" style="background-color:#{{v.namecolor}}"{% endif %}>{{v.username}}</span></a></td>
|
||||
<td>{{v.post_count}}</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
</table>
|
||||
<pre>
|
||||
|
||||
|
||||
</pre>
|
||||
<h5 style="font-weight:bold;text-align: center">Top 25 by comment count</h5>
|
||||
<pre>
|
||||
|
||||
|
||||
</pre>
|
||||
<div class="overflow-x-auto"><table class="table table-striped mb-5">
|
||||
<thead class="bg-primary text-white">
|
||||
<tr>
|
||||
<th>#</th>
|
||||
<th>Name</th>
|
||||
<th>Comments</th>
|
||||
</tr>
|
||||
</thead>
|
||||
{% for user in users4 %}
|
||||
<tr {% if v.id == user.id %}class="self"{% endif %}>
|
||||
<td>{{loop.index}}</td>
|
||||
<td><a style="color:#{{user.namecolor}};font-weight:bold" href="/@{{user.username}}"><img loading="lazy" src="{{user.profile_url}}" class="pp20"><span {% if user.patron %}class="patron" style="background-color:#{{user.namecolor}}"{% endif %}>{{user.username}}</span></a></td>
|
||||
<td>{{user.comment_count}}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% if pos4 > 25 %}
|
||||
<tr style="border-top:2px solid var(--primary)">
|
||||
<td>{{pos4}}</td>
|
||||
<td><a style="color:#{{v.namecolor}};font-weight:bold" href="/@{{v.username}}"><img loading="lazy" src="{{v.profile_url}}" class="pp20"><span {% if v.patron %}class="patron" style="background-color:#{{v.namecolor}}"{% endif %}>{{v.username}}</span></a></td>
|
||||
<td>{{v.comment_count}}</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
</table>
|
||||
|
||||
|
||||
<pre>
|
||||
|
||||
|
||||
</pre>
|
||||
<h5 style="font-weight:bold;text-align: center">Top 25 by received awards</h5>
|
||||
<pre>
|
||||
|
||||
|
||||
</pre>
|
||||
<div class="overflow-x-auto"><table class="table table-striped mb-5">
|
||||
<thead class="bg-primary text-white">
|
||||
<tr>
|
||||
<th>#</th>
|
||||
<th>Name</th>
|
||||
<th>Awards</th>
|
||||
</tr>
|
||||
</thead>
|
||||
{% for user in users5 %}
|
||||
<tr {% if v.id == user.id %}class="self"{% endif %}>
|
||||
<td>{{loop.index}}</td>
|
||||
<td><a style="color:#{{user.namecolor}};font-weight:bold" href="/@{{user.username}}"><img loading="lazy" src="{{user.profile_url}}" class="pp20"><span {% if user.patron %}class="patron" style="background-color:#{{user.namecolor}}"{% endif %}>{{user.username}}</span></a></td>
|
||||
<td>{{user.received_award_count}}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% if pos5 > 25 %}
|
||||
<tr style="border-top:2px solid var(--primary)">
|
||||
<td>{{pos5}}</td>
|
||||
<td><a style="color:#{{v.namecolor}};font-weight:bold" href="/@{{v.username}}"><img loading="lazy" src="{{v.profile_url}}" class="pp20"><span {% if v.patron %}class="patron" style="background-color:#{{v.namecolor}}"{% endif %}>{{v.username}}</span></a></td>
|
||||
<td>{{v.received_award_count}}</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
</table>
|
||||
|
||||
|
||||
<pre>
|
||||
|
||||
|
||||
</pre>
|
||||
<h5 style="font-weight:bold;text-align: center">Top 25 by received downvotes</h5>
|
||||
<pre>
|
||||
|
||||
|
||||
</pre>
|
||||
|
||||
<div class="overflow-x-auto"><table class="table table-striped mb-5">
|
||||
<thead class="bg-primary text-white">
|
||||
<tr>
|
||||
<th>#</th>
|
||||
<th>Name</th>
|
||||
<th>Downvotes</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="followers-table">
|
||||
{% for user in users9 %}
|
||||
<tr {% if v.id == user[0].id %}class="self"{% endif %}>
|
||||
<td>{{loop.index}}</td>
|
||||
<td><a style="color:#{{user[0].namecolor}};font-weight:bold" href="/@{{user[0].username}}"><img loading="lazy" src="{{user[0].profile_url}}" class="pp20"><span {% if user[0].patron %}class="patron" style="background-color:#{{user[0].namecolor}}"{% endif %}>{{user[0].username}}</span></a></td>
|
||||
<td>{{user[1]}}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% if pos9 and (pos9[0] > 25 or not pos9[1]) %}
|
||||
<tr style="border-top:2px solid var(--primary)">
|
||||
<td>{{pos9[0]}}</td>
|
||||
<td><a style="color:#{{v.namecolor}};font-weight:bold" href="/@{{v.username}}"><img loading="lazy" src="{{v.profile_url}}" class="pp20"><span {% if v.patron %}class="patron" style="background-color:#{{v.namecolor}}"{% endif %}>{{v.username}}</span></a></td>
|
||||
<td>{{pos9[1]}}</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
|
||||
<pre>
|
||||
|
||||
|
||||
</pre>
|
||||
<h5 style="font-weight:bold;text-align: center">Top 25 by badges</h5>
|
||||
<pre>
|
||||
|
||||
|
||||
</pre>
|
||||
|
||||
<div class="overflow-x-auto"><table class="table table-striped mb-5">
|
||||
<thead class="bg-primary text-white">
|
||||
<tr>
|
||||
<th>#</th>
|
||||
<th>Name</th>
|
||||
<th>Badges</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="followers-table">
|
||||
{% for user in users11 %}
|
||||
<tr {% if v.id == user[0].id %}class="self"{% endif %}>
|
||||
<td>{{loop.index}}</td>
|
||||
<td><a style="color:#{{user[0].namecolor}};font-weight:bold" href="/@{{user[0].username}}"><img loading="lazy" src="{{user[0].profile_url}}" class="pp20"><span {% if user[0].patron %}class="patron" style="background-color:#{{user[0].namecolor}}"{% endif %}>{{user[0].username}}</span></a></td>
|
||||
<td>{{user[1]}}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% if pos11 and (pos11[0] > 25 or not pos11[1]) %}
|
||||
<tr style="border-top:2px solid var(--primary)">
|
||||
<td>{{pos11[0]}}</td>
|
||||
<td><a style="color:#{{v.namecolor}};font-weight:bold" href="/@{{v.username}}"><img loading="lazy" src="{{v.profile_url}}" class="pp20"><span {% if v.patron %}class="patron" style="background-color:#{{v.namecolor}}"{% endif %}>{{v.username}}</span></a></td>
|
||||
<td>{{pos11[1]}}</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
|
||||
|
||||
{% if users6 %}
|
||||
<pre>
|
||||
|
||||
|
||||
</pre>
|
||||
<h5 style="font-weight:bold;text-align: center">Top 25 by based count</h5>
|
||||
<pre>
|
||||
|
||||
|
||||
</pre>
|
||||
<div class="overflow-x-auto"><table class="table table-striped mb-5">
|
||||
<thead class="bg-primary text-white">
|
||||
<tr>
|
||||
<th>#</th>
|
||||
<th>Name</th>
|
||||
<th>Based count</th>
|
||||
</tr>
|
||||
</thead>
|
||||
{% for user in users6 %}
|
||||
<tr {% if v.id == user.id %}class="self"{% endif %}>
|
||||
<td>{{loop.index}}</td>
|
||||
<td><a style="color:#{{user.namecolor}};font-weight:bold" href="/@{{user.username}}"><img loading="lazy" src="{{user.profile_url}}" class="pp20"><span {% if user.patron %}class="patron" style="background-color:#{{user.namecolor}}"{% endif %}>{{user.username}}</span></a></td>
|
||||
<td>{{user.basedcount}}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% if pos6 > 25 %}
|
||||
<tr style="border-top:2px solid var(--primary)">
|
||||
<td>{{pos6}}</td>
|
||||
<td><a style="color:#{{v.namecolor}};font-weight:bold" href="/@{{v.username}}"><img loading="lazy" src="{{v.profile_url}}" class="pp20"><span {% if v.patron %}class="patron" style="background-color:#{{v.namecolor}}"{% endif %}>{{v.username}}</span></a></td>
|
||||
<td>{{v.basedcount}}</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
</table>
|
||||
{% endif %}
|
||||
|
||||
{% if users12 %}
|
||||
<pre>
|
||||
|
||||
|
||||
</pre>
|
||||
<h5 style="font-weight:bold;text-align: center">Top 25 by marseys made</h5>
|
||||
<pre>
|
||||
|
||||
|
||||
</pre>
|
||||
|
||||
<div class="overflow-x-auto"><table class="table table-striped mb-5">
|
||||
<thead class="bg-primary text-white">
|
||||
<tr>
|
||||
<th>#</th>
|
||||
<th>Name</th>
|
||||
<th>Marseys</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="followers-table">
|
||||
{% for user in users12 %}
|
||||
<tr {% if v.id == user[0].id %}class="self"{% endif %}>
|
||||
<td>{{loop.index}}</td>
|
||||
<td><a style="color:#{{user[0].namecolor}};font-weight:bold" href="/@{{user[0].username}}"><img loading="lazy" src="{{user[0].profile_url}}" class="pp20"><span {% if user[0].patron %}class="patron" style="background-color:#{{user[0].namecolor}}"{% endif %}>{{user[0].username}}</span></a></td>
|
||||
<td>{{user[1]}}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% if pos12 and (pos12[0] > 25 or not pos12[1]) %}
|
||||
<tr style="border-top:2px solid var(--primary)">
|
||||
<td>{{pos12[0]}}</td>
|
||||
<td><a style="color:#{{v.namecolor}};font-weight:bold" href="/@{{v.username}}"><img loading="lazy" src="{{v.profile_url}}" class="pp20"><span {% if v.patron %}class="patron" style="background-color:#{{v.namecolor}}"{% endif %}>{{v.username}}</span></a></td>
|
||||
<td>{{pos12[1]}}</td>
|
||||
</tr>
|
||||
<div id="leaderboard-contents" style="text-align: center; margin-bottom: 1.5rem; font-size: 1.2rem;">
|
||||
{% for lb in leaderboards %}
|
||||
{% if lb %}
|
||||
<a href="#leaderboard-{{lb.html_id}}">{{lb.meta.header_name}}</a>{% if not loop.last %} •{% endif %}
|
||||
{% endif %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
{% if users13 %}
|
||||
<pre>
|
||||
|
||||
|
||||
</pre>
|
||||
<h5 style="font-weight:bold;text-align: center">Top 25 by upvotes given</h5>
|
||||
<pre>
|
||||
|
||||
|
||||
</pre>
|
||||
|
||||
<div class="overflow-x-auto"><table class="table table-striped mb-5">
|
||||
<thead class="bg-primary text-white">
|
||||
<tr>
|
||||
<th>#</th>
|
||||
<th>Name</th>
|
||||
<th>Upvotes</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="followers-table">
|
||||
{% for user in users13 %}
|
||||
<tr {% if v.id == user[0].id %}class="self"{% endif %}>
|
||||
<td>{{loop.index}}</td>
|
||||
<td><a style="color:#{{user[0].namecolor}};font-weight:bold" href="/@{{user[0].username}}"><img loading="lazy" src="{{user[0].profile_url}}" class="pp20"><span {% if user[0].patron %}class="patron" style="background-color:#{{user[0].namecolor}}"{% endif %}>{{user[0].username}}</span></a></td>
|
||||
<td>{{user[1]}}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% if pos13 and (pos13[0] > 25 or not pos13[1]) %}
|
||||
<tr style="border-top:2px solid var(--primary)">
|
||||
<td>{{pos13[0]}}</td>
|
||||
<td><a style="color:#{{v.namecolor}};font-weight:bold" href="/@{{v.username}}"><img loading="lazy" src="{{v.profile_url}}" class="pp20"><span {% if v.patron %}class="patron" style="background-color:#{{v.namecolor}}"{% endif %}>{{v.username}}</span></a></td>
|
||||
<td>{{pos13[1]}}</td>
|
||||
</tr>
|
||||
{% macro format_user_in_table(user, style, position_no, value, user_relative_url) %}
|
||||
{% set value = value | int %}
|
||||
<tr {{style | safe}}>
|
||||
<td>{{position_no}}</td>
|
||||
<td>{% include "user_in_table.html" %}</td>
|
||||
{% if user_relative_url is not none %}
|
||||
<td><a href="/@{{user.username}}/{{user_relative_url}}">{{"{:,}".format(value)}}</a></td>
|
||||
{% else %}
|
||||
<td>{{"{:,}".format(value)}}</td>
|
||||
{% endif %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% endif %}
|
||||
</tr>
|
||||
{% endmacro %}
|
||||
|
||||
<h5 style="font-weight:bold;text-align: center">Top 25 by winnings</h5>
|
||||
<pre></pre>
|
||||
<div class="overflow-x-auto"><table class="table table-striped mb-5">
|
||||
<thead class="bg-primary text-white">
|
||||
<tr>
|
||||
<th>#</th>
|
||||
<th>Name</th>
|
||||
<th>Winnings</th>
|
||||
</tr>
|
||||
</thead>
|
||||
{% for user in users14 %}
|
||||
<tr {% if v.id == user.id %}class="self"{% endif %}>
|
||||
<td>{{loop.index}}</td>
|
||||
<td><a style="color:#{{user.namecolor}};font-weight:bold" href="/@{{user.username}}"><img loading="lazy" src="{{user.profile_url}}" class="pp20"><span {% if user.patron %}class="patron" style="background-color:#{{user.namecolor}}"{% endif %}>{{user.username}}</span></a></td>
|
||||
<td>{{user.winnings}}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% if pos14 > 25 %}
|
||||
<tr style="border-top:2px solid var(--primary)">
|
||||
<td>{{pos14}}</td>
|
||||
<td><a style="color:#{{v.namecolor}};font-weight:bold" href="/@{{v.username}}"><img loading="lazy" src="{{v.profile_url}}" class="pp20"><span {% if v.patron %}class="patron" style="background-color:#{{v.namecolor}}"{% endif %}>{{v.username}}</span></a></td>
|
||||
<td>{{v.winnings}}</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% macro leaderboard_table(lb) %}
|
||||
<h5 class="font-weight-bolder text-center pt-2 pb-3"><span id="leaderboard-{{lb.meta.html_id}}">Top {{lb.limit}} {% if lb.meta.table_header_name != 'most blocked' %}by{% endif %} {{lb.meta.table_header_name}}</span></h5>
|
||||
<div class="overflow-x-auto">
|
||||
<table class="table table-striped mb-5">
|
||||
<thead class="bg-primary text-white">
|
||||
<tr>
|
||||
<th>#</th>
|
||||
<th>Name</th>
|
||||
<th>{{lb.meta.table_column_name}}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for user in lb.all_users %}
|
||||
{% set user2 = lb.user_func(user) %}
|
||||
{% if v.id == user2.id %}
|
||||
{% set style="class=\"self\"" %}
|
||||
{% endif %}
|
||||
{{format_user_in_table(user2, style, loop.index, lb.value_func(user), lb.meta.user_relative_url)}}
|
||||
{% endfor %}
|
||||
{% if lb.v_position and not lb.v_appears_in_ranking %}
|
||||
{{format_user_in_table(v, "style=\"border-top:2px solid var(--primary)\"", lb.v_position, lb.v_value, lb.meta.user_relative_url)}}
|
||||
{% endif %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
|
||||
<pre>
|
||||
|
||||
|
||||
</pre>
|
||||
<h5 style="font-weight:bold;text-align: center">Bottom 25 by winnings</h5>
|
||||
<pre></pre>
|
||||
<div class="overflow-x-auto"><table class="table table-striped mb-5">
|
||||
<thead class="bg-primary text-white">
|
||||
<tr>
|
||||
<th>#</th>
|
||||
<th>Name</th>
|
||||
<th>Winnings</th>
|
||||
</tr>
|
||||
</thead>
|
||||
{% for user in users15 %}
|
||||
<tr {% if v.id == user.id %}class="self"{% endif %}>
|
||||
<td>{{loop.index}}</td>
|
||||
<td><a style="color:#{{user.namecolor}};font-weight:bold" href="/@{{user.username}}"><img loading="lazy" src="{{user.profile_url}}" class="pp20"><span {% if user.patron %}class="patron" style="background-color:#{{user.namecolor}}"{% endif %}>{{user.username}}</span></a></td>
|
||||
<td>{{user.winnings}}</td>
|
||||
</tr>
|
||||
{% for lb in leaderboards %}
|
||||
{% if lb %}
|
||||
{{leaderboard_table(lb)}}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% if pos15 > 25 %}
|
||||
<tr style="border-top:2px solid var(--primary)">
|
||||
<td>{{pos15}}</td>
|
||||
<td><a style="color:#{{v.namecolor}};font-weight:bold" href="/@{{v.username}}"><img loading="lazy" src="{{v.profile_url}}" class="pp20"><span {% if v.patron %}class="patron" style="background-color:#{{v.namecolor}}"{% endif %}>{{v.username}}</span></a></td>
|
||||
<td>{{v.winnings}}</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
</table>
|
||||
|
||||
|
||||
<pre>
|
||||
|
||||
|
||||
</pre>
|
||||
<a id="leader--top-btn" href="#leaderboard-contents"
|
||||
style="position: fixed; bottom: 5rem; right: 2rem; font-size: 3rem;">
|
||||
<i class="fas fa-arrow-alt-circle-up"></i>
|
||||
</a>
|
||||
{% endblock %}
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
{% extends "home.html" %}
|
||||
{%- import 'component/sorting_time.html' as sorting_time -%}
|
||||
|
||||
{% block pagetype %}search{% endblock %}
|
||||
|
||||
|
@ -29,38 +30,15 @@
|
|||
{% if t != "all" %}<a class="dropdown-item" href="?q={{query | urlencode}}&sort={{sort}}&t=all"><i class="fas fa-infinity mr-2"></i>All</a>{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="dropdown dropdown-actions">
|
||||
<button class="btn btn-secondary dropdown-toggle" role="button" id="dropdownMenuButton2" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
{% if sort=="top" %}<i class="fas fa-arrow-alt-circle-up mr-1"></i>{% endif %}
|
||||
{% if sort=="bottom" %}<i class="fas fa-arrow-alt-circle-down mr-1"></i>{% endif %}
|
||||
{% if sort=="new" %}<i class="fas fa-sparkles mr-1"></i>{% endif %}
|
||||
{% if sort=="old" %}<i class="fas fa-book mr-1"></i>{% endif %}
|
||||
{% if sort=="controversial" %}<i class="fas fa-bullhorn mr-1"></i>{% endif %}
|
||||
{% if sort=="comments" %}<i class="fas fa-comments mr-1"></i>{% endif %}
|
||||
{{sort | capitalize}}
|
||||
</button>
|
||||
<div class="dropdown-menu" aria-labelledby="dropdownMenuButton2" x-placement="bottom-start" style="position: absolute; will-change: transform; top: 0px; left: 0px; transform: translate3d(0px, 31px, 0px);">
|
||||
{% if sort != "top" %}<a class="dropdown-item" href="?q={{query | urlencode}}&sort=top&t={{t}}"><i class="fas fa-arrow-alt-circle-up mr-2"></i>Top</a>{% endif %}
|
||||
{% if sort != "bottom" %}<a class="dropdown-item" href="?q={{query | urlencode}}&sort=bottom&t={{t}}"><i class="fas fa-arrow-alt-circle-down mr-2"></i>Bottom</a>{% endif %}
|
||||
{% if sort != "new" %}<a class="dropdown-item" href="?q={{query | urlencode}}&sort=new&t={{t}}"><i class="fas fa-sparkles mr-2"></i>New</a>{% endif %}
|
||||
{% if sort != "old" %}<a class="dropdown-item" href="?q={{query | urlencode}}&sort=old&t={{t}}"><i class="fas fa-book mr-2"></i>Old</a>{% endif %}
|
||||
{% if sort != "controversial" %}<a class="dropdown-item" href="?q={{query | urlencode}}&sort=controversial&t={{t}}"><i class="fas fa-bullhorn mr-2"></i>Controversial</a>{% endif %}
|
||||
{% if sort != "comments" and "/posts/" in request.path %}<a class="dropdown-item" href="?q={{query | urlencode}}&sort=comments&t={{t}}"><i class="fas fa-comments mr-2"></i>Comments</a>{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% set query_text = 'q=' ~ query | urlencode %}
|
||||
{{sorting_time.sort_dropdown(sort, t, SORTS_POSTS, query_text)}}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<div class="row no-gutters my-md-3">
|
||||
|
||||
<div class="col">
|
||||
|
||||
<div class="card search-results">
|
||||
|
||||
<div class="card-header bg-white d-none">
|
||||
<ul class="list-inline no-bullets mb-0">
|
||||
<li class="list-inline-item active mr-4"><i class="fas fa-align-left text-gray-400"></i></li>
|
||||
|
@ -73,91 +51,47 @@
|
|||
<br>
|
||||
<div class="text-muted text-small mb-1">Showing {% block listinglength %}{{listing | length}}{% endblock %} of {{total}} result{{'s' if total != 1 else ''}} for</div>
|
||||
<h1 class="h4 mb-0">{{query}}</h1>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
{% if not '/users/' in request.path %}
|
||||
|
||||
<div class="flex-row tab-bar d-none">
|
||||
|
||||
<ul class="nav post-nav mr-auto">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link{% if sort=='top' %} active{% endif %}" href="?sort=top&q={{query | urlencode}}&t={{t}}"><i class="fas fa-arrow-alt-circle-up"></i>Top</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link{% if sort=='bottom' %} active{% endif %}" href="?sort=bottom&q={{query | urlencode}}&t={{t}}"><i class="fas fa-arrow-alt-circle-down"></i>Bottom</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link{% if sort=='new' %} active{% endif %}" href="?sort=new&q={{query | urlencode}}&t={{t}}"><i class="fas fa-sparkles"></i>New</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link{% if sort=='old' %} active{% endif %}" href="?sort=old&q={{query | urlencode}}&t={{t}}"><i class="fas fa-book"></i>Old</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link{% if sort=='fiery' %} active{% endif %}" href="?sort=fiery&q={{query | urlencode}}&t={{t}}"><i class="fas fa-bullhorn"></i>Controversial</a>
|
||||
</li>
|
||||
<li class="nav-item ">
|
||||
<a class="nav-link {% if sort=='comments' %} active{% endif %}" href="/?sort=comments&q={{query | urlencode}}&t={{t}}"><i class="fas fa-comments"></i>Comments</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link{% if sort=='random' %} active{% endif %}" href="?sort=random&q={{query | urlencode}}&t={{t}}"><i class="fas fa-arrow-alt-circle-down"></i>Random</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="row no-gutters">
|
||||
<div class="col">
|
||||
<div class="flex-row box-shadow-bottom d-flex justify-content-center justify-content-md-between align-items-center">
|
||||
<ul class="nav settings-nav">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link{% if '/posts/' in request.path %} active{% endif %}" href="/search/posts/?sort={{sort}}&q={{query | urlencode}}&t={{t}}">Posts</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link{% if '/comments/' in request.path %} active{% endif %}" href="/search/comments/?sort={{sort}}&q={{query | urlencode}}&t={{t}}">Comments</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link{% if '/users/' in request.path %} active{% endif %}" href="/search/users/?sort={{sort}}&q={{query | urlencode}}&t={{t}}">Users</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row no-gutters">
|
||||
|
||||
<div class="col-12">
|
||||
|
||||
<div class="posts" id="posts">
|
||||
|
||||
{% block listing_template %}
|
||||
{% include "submission_listing.html" %}
|
||||
{% endblock %}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block pagenav %}
|
||||
<nav aria-label="Page navigation">
|
||||
<ul class="pagination pagination-sm mb-0">
|
||||
<li class="page-item{% if page==1 %} disabled{% endif %}">
|
||||
|
||||
<small><a class="page-link" href="?sort={{sort}}&q={{query | urlencode}}&t={{t}}&page={{page-1}}" tabindex="-1"{% if page==1 %} aria-disabled="true"{% endif %}>Back</a></small>
|
||||
<div class="row no-gutters">
|
||||
<div class="col">
|
||||
<div class="flex-row box-shadow-bottom d-flex justify-content-center justify-content-md-between align-items-center">
|
||||
<ul class="nav settings-nav">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link{% if '/posts/' in request.path %} active{% endif %}" href="/search/posts/?sort={{sort}}&q={{query | urlencode}}&t={{t}}">Posts</a>
|
||||
</li>
|
||||
<li class="page-item{% if not next_exists %} disabled{% endif %}">
|
||||
<small><a class="page-link" href="?sort={{sort}}&q={{query | urlencode}}&t={{t}}&page={{page+1}}">Next</a></small>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link{% if '/comments/' in request.path %} active{% endif %}" href="/search/comments/?sort={{sort}}&q={{query | urlencode}}&t={{t}}">Comments</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link{% if '/users/' in request.path %} active{% endif %}" href="/search/users/?sort={{sort}}&q={{query | urlencode}}&t={{t}}">Users</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
{% endblock %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row no-gutters">
|
||||
<div class="col-12">
|
||||
<div class="posts" id="posts">
|
||||
{% block listing_template %}
|
||||
{% include "submission_listing.html" %}
|
||||
{% endblock %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
{% block pagenav %}
|
||||
<nav aria-label="Page navigation">
|
||||
<ul class="pagination pagination-sm mb-0">
|
||||
<li class="page-item{% if page==1 %} disabled{% endif %}">
|
||||
<small><a class="page-link" href="?sort={{sort}}&q={{query | urlencode}}&t={{t}}&page={{page-1}}" tabindex="-1"{% if page==1 %} aria-disabled="true"{% endif %}>Back</a></small>
|
||||
</li>
|
||||
<li class="page-item{% if not next_exists %} disabled{% endif %}">
|
||||
<small><a class="page-link" href="?sort={{sort}}&q={{query | urlencode}}&t={{t}}&page={{page+1}}">Next</a></small>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
{% endblock %}
|
||||
|
|
|
@ -1,24 +1,15 @@
|
|||
{% extends "settings.html" %}
|
||||
|
||||
{% block pagetitle %}Profile Settings - {{SITE_TITLE}}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
|
||||
<div class="row">
|
||||
|
||||
<div class="col col-lg-8">
|
||||
|
||||
<div class="settings">
|
||||
|
||||
<h2 class="h5" name="referral">Frontpage Size</h2>
|
||||
|
||||
<div class="settings-section rounded">
|
||||
<div class="d-lg-flex border-bottom">
|
||||
<div class="title w-lg-25">
|
||||
<label for="frontsize">Frontpage Size</label>
|
||||
</div>
|
||||
|
||||
<div class="body w-lg-100">
|
||||
<p>Change how many posts appear on every page.</p>
|
||||
<div class="input-group mb2">
|
||||
|
@ -28,9 +19,7 @@
|
|||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -48,14 +37,12 @@
|
|||
<p>Change the default sorting for comments.</p>
|
||||
<div class="input-group mb2">
|
||||
<select autocomplete="off" id='defaultsortingcomments' class="form-control" form="profile-settings" name="defaultsortingcomments" onchange="post_toast(this,'/settings/profile?defaultsortingcomments='+document.getElementById('defaultsortingcomments').value)">
|
||||
{% for entry in ["new", "old", "top", "bottom", "controversial"] %}
|
||||
{% for entry in SORTS_COMMENTS %}
|
||||
<option value="{{entry}}"{{' selected' if v.defaultsortingcomments==entry}}>{{entry}}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="d-lg-flex border-bottom">
|
||||
|
@ -67,21 +54,17 @@
|
|||
<p>Change the default sorting for posts.</p>
|
||||
<div class="input-group mb2">
|
||||
<select autocomplete="off" id='defaultsorting' class="form-control" form="profile-settings" name="defaultsorting" onchange="post_toast(this,'/settings/profile?defaultsorting='+document.getElementById('defaultsorting').value)">
|
||||
{% for entry in ["hot", "bump", "new", "old", "top", "bottom", "controversial", "comments"] %}
|
||||
{% for entry in SORTS_POSTS %}
|
||||
<option value="{{entry}}"{{' selected' if v.defaultsorting==entry}}>{{entry}}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="d-lg-flex border-bottom">
|
||||
<div class="title w-lg-25">
|
||||
<label for="defaulttime">Default Time Filter for Posts</label>
|
||||
</div>
|
||||
|
||||
<div class="body w-lg-100">
|
||||
<p>Change the default time filter for posts.</p>
|
||||
<div class="input-group mb2">
|
||||
|
@ -91,34 +74,24 @@
|
|||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<h2 class="h5">Tab Behaviour</h2>
|
||||
|
||||
<div class="settings-section rounded">
|
||||
|
||||
<div class="d-lg-flex border-bottom">
|
||||
|
||||
<div class="title w-lg-25">
|
||||
<label for="newtab">Open Internal Links In New Tabs</label>
|
||||
</div>
|
||||
|
||||
<div class="body w-lg-100">
|
||||
|
||||
<div class="custom-control custom-switch">
|
||||
<input autocomplete="off" type="checkbox" class="custom-control-input" id="newtab" name="newtab"{% if v.newtab %} checked{% endif %} onchange="post_toast(this,'/settings/profile?newtab='+document.getElementById('newtab').checked);">
|
||||
<label class="custom-control-label" for="newtab"></label>
|
||||
</div>
|
||||
|
||||
<span class="text-small-extra text-muted">Enable if you would like to automatically open links to other pages in the site in new tabs.</span>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
|
@ -136,51 +109,34 @@
|
|||
</div>
|
||||
|
||||
<span class="text-small-extra text-muted">Enable if you would like to automatically open links to other sites in new tabs.</span>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<h2 class="h5">Twitter Links</h2>
|
||||
|
||||
<div class="settings-section rounded">
|
||||
|
||||
<div class="d-lg-flex border-bottom">
|
||||
|
||||
<div class="title w-lg-25">
|
||||
<label for="nitter">Use Nitter</label>
|
||||
</div>
|
||||
|
||||
<div class="body w-lg-100">
|
||||
|
||||
<div class="custom-control custom-switch">
|
||||
<input autocomplete="off" type="checkbox" class="custom-control-input" id="nitter" name="nitter"{% if v.nitter %} checked{% endif %} onchange="post_toast(this,'/settings/profile?nitter='+document.getElementById('nitter').checked);">
|
||||
<label class="custom-control-label" for="nitter"></label>
|
||||
</div>
|
||||
|
||||
<span class="text-small-extra text-muted">Enable if you would like to automatically convert twitter.com links to nitter.net links.</span>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<h2 class="h5">Reddit Links</h2>
|
||||
|
||||
<div class="settings-section rounded">
|
||||
|
||||
<div class="d-lg-flex border-bottom">
|
||||
<div class="title w-lg-25">
|
||||
<label for="reddit">Reddit Domain</label>
|
||||
</div>
|
||||
|
||||
<div class="body w-lg-100">
|
||||
<p>Change the domain you would like to view reddit posts in.</p>
|
||||
<div class="input-group mb2">
|
||||
|
@ -190,28 +146,21 @@
|
|||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="d-lg-flex border-bottom">
|
||||
|
||||
<div class="title w-lg-25">
|
||||
<label for="controversial">Sort by Controversial</label>
|
||||
</div>
|
||||
|
||||
<div class="body w-lg-100">
|
||||
|
||||
<div class="custom-control custom-switch">
|
||||
<input autocomplete="off" type="checkbox" class="custom-control-input" id="controversial" name="controversial"{% if v.controversial %} checked{% endif %} onchange="post_toast(this,'/settings/profile?controversial='+document.getElementById('controversial').checked);">
|
||||
<label class="custom-control-label" for="controversial"></label>
|
||||
</div>
|
||||
|
||||
<span class="text-small-extra text-muted">Enable if you would like to automatically sort reddit.com links by controversial.</span>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -384,7 +384,7 @@
|
|||
|
||||
<label class="btn btn-secondary format d-inline-block m-0">
|
||||
<div id="filename-show"><i class="far fa-image"></i></div>
|
||||
<input autocomplete="off" id="file-upload" type="file" name="file" {% if request.headers.get('cf-ipcountry')=="T1" %}disabled{% endif %} accept="image/*, video/*" onchange="changename('filename-show','file-upload')" hidden>
|
||||
<input autocomplete="off" id="file-upload" type="file" name="file" {% if request.headers.get('cf-ipcountry')=="T1" %}disabled{% endif %} accept="image/*" onchange="changename('filename-show','file-upload')" hidden>
|
||||
</label>
|
||||
</div>
|
||||
<pre></pre>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
{% extends "default.html" %}
|
||||
|
||||
{%- import 'component/sorting_time.html' as sorting_time -%}
|
||||
|
||||
{% if p.should_hide_score %}
|
||||
{% set ups="" %}
|
||||
|
@ -137,7 +137,7 @@
|
|||
<div id="post-root" class="col-12">
|
||||
|
||||
<div class="card border-0 mt-3{% if p.is_banned %} banned{% endif %}{% if p.stickied %} stickied{% endif %}{% if voted==1 %} upvoted{% elif voted==-1 %} downvoted{% endif %}">
|
||||
<div id="post-{{p.id}}" class="{% if p.award_count('glowie') %}glow{% endif %} {% if p.deleted_utc %}deleted {% endif %}d-flex flex-row-reverse flex-nowrap justify-content-end">
|
||||
<div id="post-{{p.id}}" class="{% if p.deleted_utc %}deleted {% endif %}d-flex flex-row-reverse flex-nowrap justify-content-end">
|
||||
|
||||
<div id="post-content" class="{% if p.deleted_utc %}deleted {% endif %}card-block w-100 my-md-auto">
|
||||
|
||||
|
@ -164,15 +164,9 @@
|
|||
{% if v and p.filter_state == 'reported' and v.can_manage_reports() %}
|
||||
<a class="btn btn-primary" id="submission-report-button" role="button" style="padding:1px 5px; font-size:10px"onclick="document.getElementById('flaggers').classList.toggle('d-none')">{{p.active_flags(v)}} Reports</a>
|
||||
{% endif %}
|
||||
|
||||
{% if not p.author %}
|
||||
{{p.print()}}
|
||||
{% endif %}
|
||||
|
||||
{% if p.ghost %}
|
||||
👻
|
||||
{% else %}
|
||||
|
||||
{% if p.author.verified %}<i class="fas fa-badge-check align-middle ml-1 {% if p.author.verified=='Glowiefied' %}glow{% endif %}" style="color:{% if p.author.verifiedcolor %}#{{p.author.verifiedcolor}}{% else %}var(--primary){% endif %}" data-bs-toggle="tooltip" data-bs-placement="bottom" title="{{p.author.verified}}"></i>
|
||||
{% endif %}
|
||||
|
||||
|
@ -233,7 +227,6 @@
|
|||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
|
||||
{% if p.embed_url %}
|
||||
{% if p.domain == "twitter.com" %}
|
||||
{{p.embed_url | safe}}
|
||||
|
@ -246,8 +239,6 @@
|
|||
{{p.embed_url | safe}}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
|
||||
<div id="post-text">
|
||||
{% if p.is_image %}
|
||||
<div class="row no-gutters">
|
||||
|
@ -295,7 +286,7 @@
|
|||
<a class="format btn btn-secondary" role="button" onclick="commentForm('post-edit-box-{{p.id}}');getGif()" aria-hidden="true" data-bs-toggle="modal" data-bs-target="#gifModal" data-bs-toggle="tooltip" data-bs-placement="bottom" title="Add GIF"><span class="font-weight-bolder text-uppercase">GIF</span></a>
|
||||
<label class="format btn btn-secondary m-0 ml-1 {% if v %}d-inline-block{% else %}d-none{% endif %}" for="file-upload-edit-{{p.id}}">
|
||||
<div id="filename-show-edit-{{p.id}}"><i class="far fa-image"></i></div>
|
||||
<input autocomplete="off" id="file-upload-edit-{{p.id}}" type="file" multiple="multiple" name="file" accept="image/*, video/*" {% if request.headers.get('cf-ipcountry')=="T1" %}disabled{% endif %} onchange="changename('filename-show-edit-{{p.id}}','file-upload-edit-{{p.id}}')" hidden>
|
||||
<input autocomplete="off" id="file-upload-edit-{{p.id}}" type="file" multiple="multiple" name="file" accept="image/*" {% if request.headers.get('cf-ipcountry')=="T1" %}disabled{% endif %} onchange="changename('filename-show-edit-{{p.id}}','file-upload-edit-{{p.id}}')" hidden>
|
||||
</label>
|
||||
|
||||
<small class="format d-none"><i class="fas fa-link" aria-hidden="true"></i></small>
|
||||
|
@ -342,16 +333,11 @@
|
|||
{% include 'post_actions.html' %}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
{% if v %}
|
||||
|
||||
<div id="voting" class="voting d-none d-md-block mb-auto">
|
||||
<div tabindex="0" role="button" onclick="vote('post', '{{p.id}}', '1')" class="post-{{p.id}}-up mx-auto arrow-up upvote-button post-{{p.id}}-up {% if voted==1 %}active{% endif %}"></div>
|
||||
|
||||
<div tabindex="0" role="button" onclick="vote('post', '{{p.id}}', '1')" class="post-{{p.id}}-up mx-auto arrow-up upvote-button post-{{p.id}}-up {% if voted==1 %}active{% endif %}"></div>
|
||||
<span class="post-score-{{p.id}} score post-score-{{p.id}} {% if voted==1 %}score-up{% elif voted==-1%}score-down{% endif %}{% if p.controversial %} controversial{% endif %}" data-bs-toggle="tooltip" data-bs-placement="right" title="+{{ups}} | -{{downs}}">{{score}}</span>
|
||||
<div {% if environ.get('DISABLE_DOWNVOTES') == '1' %}style="display:None!important"{% endif %} tabindex="0" role="button" onclick="vote('post', '{{p.id}}', '-1')" class="post-{{p.id}}-down text-muted mx-auto arrow-down downvote-button post-{{p.id}}-down {% if voted==-1 %}active{% endif %}"></div>
|
||||
</div>
|
||||
|
@ -364,16 +350,9 @@
|
|||
<span class="post-{{p.id}}-score-none score text-muted{% if p.controversial %} controversial{% endif %}"{% if not p.is_banned %} data-bs-toggle="tooltip" data-bs-placement="right" title="+{{ups}} | -{{downs}}"{% endif %}>{{score}}</span>
|
||||
<div {% if environ.get('DISABLE_DOWNVOTES') == '1' %}style="display:None!important"{% endif %} tabindex="0" role="button" onclick="vote('post', '{{p.id}}', '-1')" class="post-{{p.id}}-down arrow-down mx-auto" onclick="location.href='/login?redirect={{request.path | urlencode}}';"></div>
|
||||
</div>
|
||||
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
{% if not p.is_image and not p.is_video %}
|
||||
|
@ -387,10 +366,8 @@
|
|||
|
||||
<div class="row mb-3 d-md-none">
|
||||
<div class="col-12">
|
||||
|
||||
<div class="post-actions">
|
||||
<ul class="list-inline text-right d-flex">
|
||||
|
||||
<li class="list-inline-item mr-auto">
|
||||
<a href="{{p.permalink}}">
|
||||
<i class="fas fa-comment-dots"></i>{{p.comment_count}}
|
||||
|
@ -433,9 +410,7 @@
|
|||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
{% if v and v.id != p.author_id and p.body %}
|
||||
|
@ -445,27 +420,12 @@
|
|||
<div class="row border-md-0 comment-section pb-3">
|
||||
<div class="col border-top">
|
||||
<div class="comments-count py-3">
|
||||
<div class="dropdown dropdown-actions">
|
||||
<button class="btn btn-secondary dropdown-toggle" id="dropdownMenuButton" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
{% if sort=="top" %}<i class="fas fa-arrow-alt-circle-up mr-1"></i>{% endif %}
|
||||
{% if sort=="bottom" %}<i class="fas fa-arrow-alt-circle-down mr-1"></i>{% endif %}
|
||||
{% if sort=="new" %}<i class="fas fa-sparkles mr-1"></i>{% endif %}
|
||||
{% if sort=="old" %}<i class="fas fa-book mr-1"></i>{% endif %}
|
||||
{% if sort=="controversial" %}<i class="fas fa-bullhorn mr-1"></i>{% endif %}
|
||||
{{sort | capitalize}}
|
||||
</button>
|
||||
<div class="dropdown-menu" aria-labelledby="dropdownMenuButton" x-placement="bottom-start" style="position: absolute; will-change: transform; top: 0px; left: 0px; transform: translate3d(0px, 31px, 0px);">
|
||||
{% if sort != "top" %}<a class="dropdown-item" href="?sort=top"><i class="fas fa-arrow-alt-circle-up mr-2"></i>Top</a>{% endif %}
|
||||
{% if sort != "bottom" %}<a class="dropdown-item" href="?sort=bottom"><i class="fas fa-arrow-alt-circle-down mr-2"></i>Bottom</a>{% endif %}
|
||||
{% if sort != "new" %}<a class="dropdown-item" href="?sort=new"><i class="fas fa-sparkles mr-2"></i>New</a>{% endif %}
|
||||
{% if sort != "old" %}<a class="dropdown-item" href="?sort=old"><i class="fas fa-book mr-2"></i>Old</a>{% endif %}
|
||||
{% if sort != "controversial" %}<a class="dropdown-item" href="?sort=controversial"><i class="fas fa-bullhorn mr-2"></i>Controversial</a>{% endif %}
|
||||
</div>
|
||||
{% if comment_info and p.comment_count >= 2%}
|
||||
<pre></pre>
|
||||
<div class="total"><a href="{{p.permalink}}">View entire discussion</a></div>
|
||||
{% endif %}
|
||||
{{sorting_time.sort_dropdown(sort, none, SORTS_COMMENTS)}}
|
||||
{% if comment_info and p.comment_count >= 2 %}
|
||||
<div class="total mt-3">
|
||||
<a href="{{p.permalink}}">View entire discussion</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% if v %}
|
||||
|
@ -487,7 +447,7 @@
|
|||
|
||||
<label class="format btn btn-secondary m-0 ml-1 {% if v %}d-inline-block{% else %}d-none{% endif %}" for="file-upload-reply-{{p.fullname}}">
|
||||
<div id="filename-show-reply-{{p.fullname}}"><i class="far fa-image"></i></div>
|
||||
<input autocomplete="off" id="file-upload-reply-{{p.fullname}}" type="file" multiple="multiple" name="file" accept="image/*, video/*" {% if request.headers.get('cf-ipcountry')=="T1" %}disabled{% endif %} onchange="changename('filename-show-reply-{{p.fullname}}','file-upload-reply-{{p.fullname}}')" hidden>
|
||||
<input autocomplete="off" id="file-upload-reply-{{p.fullname}}" type="file" multiple="multiple" name="file" accept="image/*" {% if request.headers.get('cf-ipcountry')=="T1" %}disabled{% endif %} onchange="changename('filename-show-reply-{{p.fullname}}','file-upload-reply-{{p.fullname}}')" hidden>
|
||||
</label>
|
||||
</div>
|
||||
<a id="save-reply-to-{{p.fullname}}" role="button" form="reply-to-{{p.fullname}}" class="btn btn-primary text-whitebtn ml-auto fl-r" onclick="post_comment('{{p.fullname}}', '{{p.id}}')">Comment</a>
|
||||
|
@ -606,18 +566,6 @@
|
|||
{% include "comments.html" %}
|
||||
{% endif %}
|
||||
|
||||
{% if p.award_count("shit") %}
|
||||
<script src="{{ 'js/critters.js' | asset }}"></script>
|
||||
<script src="{{ 'js/bugs.js' | asset }}"></script>
|
||||
{% endif %}
|
||||
|
||||
|
||||
{% if p.award_count("fireflies") %}
|
||||
<script src="{{ 'js/critters.js' | asset }}"></script>
|
||||
<script src="{{ 'js/fireflies.js' | asset }}"></script>
|
||||
{% endif %}
|
||||
|
||||
|
||||
<script>
|
||||
(() => {
|
||||
{% if not v or v.highlightcomments %}
|
||||
|
|
|
@ -20,10 +20,8 @@
|
|||
|
||||
{% block content %}
|
||||
<div class="mb-2 p-3">
|
||||
|
||||
<div class="col-12">
|
||||
|
||||
<div id="post-{{p.id}}" class="{% if p.award_count('glowie') %}glow{% endif %} card d-flex flex-row-reverse flex-nowrap justify-content-end border-0 p-0 {% if voted==1 %} upvoted{% elif voted==-1 %} downvoted{% endif %}">
|
||||
<div id="post-{{p.id}}" class="card d-flex flex-row-reverse flex-nowrap justify-content-end border-0 p-0 {% if voted==1 %} upvoted{% elif voted==-1 %} downvoted{% endif %}">
|
||||
<div class="card-block my-md-auto{% if p.is_banned %} banned{% endif %}">
|
||||
<div class="post-meta text-left d-md-none mb-1">{% if p.over_18 %}<span class="badge badge-danger">+18</span> {% endif %}{% if p.is_banned %}removed by @{{p.ban_reason}}{% else %}[Deleted by user]{% endif %}</div>
|
||||
<h5 class="card-title post-title text-left mb-0 mb-md-1">{{p.plaintitle(v)}}</h5>
|
||||
|
|
|
@ -162,9 +162,11 @@
|
|||
<a role="button"><i class="fas fa-hammer-crash text-danger" data-bs-toggle="tooltip" data-bs-placement="bottom" title="User was banned for this post{% if p.author.banned_by %} by @{{p.author.banned_by.username}}{% endif %}"></i></a>
|
||||
{% endif %}
|
||||
|
||||
{% for a in p.awards|reverse %}
|
||||
<i class="{{a.class_list}} px-1" data-bs-toggle="tooltip" data-bs-placement="bottom" title="{{a.title}} Award given by @{{a.user.username}}"></i>
|
||||
{% endfor %}
|
||||
{% if FEATURES['AWARDS'] %}
|
||||
{% for a in p.awards|reverse %}
|
||||
<i class="{{a.class_list}} px-1" data-bs-toggle="tooltip" data-bs-placement="bottom" title="{{a.title}} Award given by @{{a.user.username}}"></i>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
|
||||
{% if v and v.admin_level > 1 and p.author.shadowbanned %}
|
||||
<i class="fas fa-user-times text-admin" data-bs-toggle="tooltip" data-bs-placement="bottom" title="Shadowbanned by @{{p.author.shadowbanned}}"></i>
|
||||
|
@ -188,15 +190,9 @@
|
|||
|
||||
<span>
|
||||
<span data-bs-toggle="tooltip" data-bs-placement="bottom" onmouseover="timestamp('timestamp-{{p.id}}','{{p.created_utc}}')" id="timestamp-{{p.id}}">{{p.age_string}} by </span>
|
||||
|
||||
{% if not p.author %}
|
||||
{{p.print()}}
|
||||
{% endif %}
|
||||
|
||||
{% if p.ghost %}
|
||||
👻
|
||||
{% else %}
|
||||
|
||||
{% if p.author.verified %}<i class="fas fa-badge-check align-middle ml-1 {% if p.author.verified=='Glowiefied' %}glow{% endif %}" style="color:{% if p.author.verifiedcolor %}#{{p.author.verifiedcolor}}{% else %}#1DA1F2{% endif %}" data-bs-toggle="tooltip" data-bs-placement="bottom" title="{{p.author.verified}}"></i>
|
||||
{% endif %}
|
||||
<a href="/@{{p.author_name}}"
|
||||
|
|
|
@ -72,7 +72,7 @@
|
|||
<img loading="lazy" id="image-preview" style="max-width:50%">
|
||||
<label class="btn btn-secondary m-0" for="file-upload">
|
||||
<div id="filename-show">Select File</div>
|
||||
<input autocomplete="off" id="file-upload" type="file" name="file" {% if request.headers.get('cf-ipcountry')=="T1" %}disabled{% endif %} accept="image/*, video/*" hidden>
|
||||
<input autocomplete="off" id="file-upload" type="file" name="file" {% if request.headers.get('cf-ipcountry')=="T1" %}disabled{% endif %} accept="image/*" hidden>
|
||||
</label>
|
||||
<small class="form-text text-muted">Optional if you have text.</small>
|
||||
<small class="form-text text-muted">You can upload images or videos up to 60 seconds.</small>
|
||||
|
@ -110,7 +110,7 @@
|
|||
|
||||
<label class="format btn btn-secondary m-0 ml-1 {% if v %}d-inline-block{% else %}d-none{% endif %}" for="file-upload-submit">
|
||||
<div id="filename-show-submit"><i class="far fa-image"></i></div>
|
||||
<input autocomplete="off" id="file-upload-submit" multiple="multiple" type="file" name="file2" accept="image/*, video/*" {% if request.headers.get('cf-ipcountry')=="T1" %}disabled{% endif %} onchange="changename('filename-show-submit','file-upload-submit');checkForRequired()" hidden>
|
||||
<input autocomplete="off" id="file-upload-submit" multiple="multiple" type="file" name="file2" accept="image/*" {% if request.headers.get('cf-ipcountry')=="T1" %}disabled{% endif %} onchange="changename('filename-show-submit','file-upload-submit');checkForRequired()" hidden>
|
||||
</label>
|
||||
|
||||
<div id="preview" class="preview my-3"></div>
|
||||
|
|
17
files/templates/user_in_table.html
Normal file
17
files/templates/user_in_table.html
Normal file
|
@ -0,0 +1,17 @@
|
|||
{%- include 'admin/shadowbanned_tooltip.html' -%}
|
||||
{% macro username_display(user, name=None, distinguish=0, display_class="") %}
|
||||
{% set display_name = user.username %}
|
||||
{% set display_color_fg = "ffffff" if user.patron else user.name_color %}
|
||||
{% set display_color_bg = user.name_color if (user.patron and not distinguish) else None %}
|
||||
{% set display_class = "mod" if distinguish else ("rounded-bg" if user.patron else "") %}
|
||||
<span class="font-weight-bold {{display_class}}" style="color: #{{display_color_fg}};{% if display_color_bg %} background-color:#{{display_color_bg}};{% endif %}">{{display_name}}</span>
|
||||
{% endmacro %}
|
||||
|
||||
{% if user %}
|
||||
<a data-sort-key="{{user.username.lower()}}" href="/@{{user.username}}">
|
||||
<span class="profile-pic-20-wrapper">
|
||||
<img loading="lazy" src="{{user.profile_url}}" class="pp20">
|
||||
</span>
|
||||
{{username_display(user)}}
|
||||
</a>
|
||||
{% endif %}
|
|
@ -1,5 +1,5 @@
|
|||
{% extends "default.html" %}
|
||||
|
||||
{%- import 'component/sorting_time.html' as sorting_time -%}
|
||||
|
||||
{% block pagetype %}userpage{% endblock %}
|
||||
|
||||
|
@ -130,7 +130,7 @@
|
|||
{{u.enemies_html | safe}}
|
||||
{% endif %}
|
||||
|
||||
{% if u.received_awards %}
|
||||
{% if FEATURES['AWARDS'] and u.received_awards %}
|
||||
<div class="text-white rounded p-2 mb-3" style="background-color: rgba(50, 50, 50, 0.6); width: 30%;">
|
||||
<p class="text-uppercase my-0" style="font-weight: bold; font-size: 12px;">Awards received</p>
|
||||
{% for a in u.received_awards %}
|
||||
|
@ -190,10 +190,6 @@
|
|||
<a href="/views" class="btn btn-secondary">Profile views</a>
|
||||
{% endif %}
|
||||
|
||||
{% if u.song and v and (v.id == u.id) %}
|
||||
<a class="btn btn-secondary" role="button" onclick="toggle()">Toggle anthem</a>
|
||||
{% endif %}
|
||||
|
||||
{% if v and v.id != u.id and v.admin_level > 1 %}
|
||||
<br><br>
|
||||
<div class="body d-lg-flex border-bottom">
|
||||
|
@ -379,7 +375,7 @@
|
|||
{{u.enemies_html | safe}}
|
||||
{% endif %}
|
||||
|
||||
{% if u.received_awards %}
|
||||
{% if FEATURES['AWARDS'] and u.received_awards %}
|
||||
<div class="text-white rounded p-2 my-3 text-center" style="background-color: rgba(50, 50, 50, 0.6);">
|
||||
<p class="text-uppercase my-0" style="font-weight: bold; font-size: 12px;">Awards received</p>
|
||||
{% for a in u.received_awards %}
|
||||
|
@ -416,10 +412,6 @@
|
|||
<a href="/views" class="btn btn-secondary">Profile views</a>
|
||||
{% endif %}
|
||||
|
||||
{% if u.song and v and (v.id == u.id) %}
|
||||
<a class="btn btn-secondary" role="button" onclick="toggle()">Toggle anthem</a>
|
||||
{% endif %}
|
||||
|
||||
{% if v and v.id != u.id %}
|
||||
<a id="button-unsub2" class="btn btn-secondary {% if not is_following %}d-none{% endif %}" role="button" onclick="post_toast2(this,'/unfollow/{{u.username}}','button-unsub2','button-sub2')">Unfollow</a>
|
||||
|
||||
|
@ -601,25 +593,7 @@
|
|||
</div>
|
||||
|
||||
<div class="text-small font-weight-bold ml-3 mr-2"></div>
|
||||
<div class="dropdown dropdown-actions">
|
||||
<button class="btn btn-secondary dropdown-toggle" role="button" id="dropdownMenuButton2" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
{% if sort=="top" %}<i class="fas fa-arrow-alt-circle-up mr-1"></i>{% endif %}
|
||||
{% if sort=="bottom" %}<i class="fas fa-arrow-alt-circle-down mr-1"></i>{% endif %}
|
||||
{% if sort=="new" %}<i class="fas fa-sparkles mr-1"></i>{% endif %}
|
||||
{% if sort=="old" %}<i class="fas fa-book mr-1"></i>{% endif %}
|
||||
{% if sort=="controversial" %}<i class="fas fa-bullhorn mr-1"></i>{% endif %}
|
||||
{% if sort=="comments" %}<i class="fas fa-comments mr-1"></i>{% endif %}
|
||||
{{sort | capitalize}}
|
||||
</button>
|
||||
<div class="dropdown-menu" aria-labelledby="dropdownMenuButton2" x-placement="bottom-start" style="position: absolute; will-change: transform; top: 0px; left: 0px; transform: translate3d(0px, 31px, 0px);">
|
||||
{% if sort != "top" %}<a class="dropdown-item" href="?sort=top&t={{t}}"><i class="fas fa-arrow-alt-circle-up mr-2"></i>Top</a>{% endif %}
|
||||
{% if sort != "bottom" %}<a class="dropdown-item" href="?sort=bottom&t={{t}}"><i class="fas fa-arrow-alt-circle-down mr-2"></i>Bottom</a>{% endif %}
|
||||
{% if sort != "new" %}<a class="dropdown-item" href="?sort=new&t={{t}}"><i class="fas fa-sparkles mr-2"></i>New</a>{% endif %}
|
||||
{% if sort != "old" %}<a class="dropdown-item" href="?sort=old&t={{t}}"><i class="fas fa-book mr-2"></i>Old</a>{% endif %}
|
||||
{% if sort != "controversial" %}<a class="dropdown-item" href="?sort=controversial&t={{t}}"><i class="fas fa-bullhorn mr-2"></i>Controversial</a>{% endif %}
|
||||
{% if sort != "comments" %}<a class="dropdown-item" href="?sort=comments&t={{t}}"><i class="fas fa-comments mr-2"></i>Comments</a>{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{{sorting_time.sort_dropdown(sort, t, SORTS_POSTS)}}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
@ -636,22 +610,12 @@
|
|||
|
||||
</div>
|
||||
|
||||
{% if u.song %}
|
||||
{% if v and v.id == u.id %}
|
||||
<div id="v_username" class="d-none">{{v.username}}</div>
|
||||
{% else %}
|
||||
<div id="u_username" class="d-none">{{u.username}}</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
{% if v %}
|
||||
<div id='tax' class="d-none">{% if v.patron or u.patron or v.alts_patron or u.alts_patron %}0{% else %}0.03{% endif %}</div>
|
||||
<script src="{{ 'js/userpage_v.js' | asset }}"></script>
|
||||
<div id="username" class="d-none">{{u.username}}</div>
|
||||
{% endif %}
|
||||
|
||||
<script src="{{ 'js/userpage.js' | asset }}"></script>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block pagenav %}
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
{% extends "userpage.html" %}
|
||||
{%- import 'component/sorting_time.html' as sorting_time -%}
|
||||
|
||||
{% block content %}
|
||||
|
||||
|
@ -52,23 +53,7 @@
|
|||
</div>
|
||||
|
||||
<div class="text-small font-weight-bold ml-3 mr-2"></div>
|
||||
<div class="dropdown dropdown-actions">
|
||||
<button class="btn btn-secondary dropdown-toggle" role="button" id="dropdownMenuButton2" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
{% if sort=="top" %}<i class="fas fa-arrow-alt-circle-up mr-1"></i>{% endif %}
|
||||
{% if sort=="bottom" %}<i class="fas fa-arrow-alt-circle-down mr-1"></i>{% endif %}
|
||||
{% if sort=="new" %}<i class="fas fa-sparkles mr-1"></i>{% endif %}
|
||||
{% if sort=="old" %}<i class="fas fa-book mr-1"></i>{% endif %}
|
||||
{% if sort=="controversial" %}<i class="fas fa-bullhorn mr-1"></i>{% endif %}
|
||||
{{sort | capitalize}}
|
||||
</button>
|
||||
<div class="dropdown-menu" aria-labelledby="dropdownMenuButton2" x-placement="bottom-start" style="position: absolute; will-change: transform; top: 0px; left: 0px; transform: translate3d(0px, 31px, 0px);">
|
||||
{% if sort != "top" %}<a class="dropdown-item" href="?sort=top&t={{t}}"><i class="fas fa-arrow-alt-circle-up mr-2"></i>Top</a>{% endif %}
|
||||
{% if sort != "bottom" %}<a class="dropdown-item" href="?sort=bottom&t={{t}}"><i class="fas fa-arrow-alt-circle-down mr-2"></i>Bottom</a>{% endif %}
|
||||
{% if sort != "new" %}<a class="dropdown-item" href="?sort=new&t={{t}}"><i class="fas fa-sparkles mr-2"></i>New</a>{% endif %}
|
||||
{% if sort != "old" %}<a class="dropdown-item" href="?sort=old&t={{t}}"><i class="fas fa-book mr-2"></i>Old</a>{% endif %}
|
||||
{% if sort != "controversial" %}<a class="dropdown-item" href="?sort=controversial&t={{t}}"><i class="fas fa-bullhorn mr-2"></i>Controversial</a>{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{{sorting_time.sort_dropdown(sort, t, SORTS_COMMENTS)}}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
@ -104,19 +89,10 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
{% if u.song %}
|
||||
{% if v and v.id == u.id %}
|
||||
<div id="v_username" class="d-none">{{v.username}}</div>
|
||||
{% else %}
|
||||
<div id="u_username" class="d-none">{{u.username}}</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
{% if v %}
|
||||
<div id='tax' class="d-none">{% if v.patron or u.patron %}0{% else %}0.03{% endif %}</div>
|
||||
<script src="{{ 'js/userpage_v.js' | asset }}"></script>
|
||||
<div id="username" class="d-none">{{u.username}}</div>
|
||||
{% endif %}
|
||||
|
||||
<script src="{{ 'js/userpage.js' | asset }}"></script>
|
||||
{% endblock %}
|
||||
|
|
|
@ -15,20 +15,9 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
{% if u.song %}
|
||||
{% if v and v.id == u.id %}
|
||||
<div id="v_username" class="d-none">{{v.username}}</div>
|
||||
{% else %}
|
||||
<div id="u_username" class="d-none">{{u.username}}</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block pagenav %}
|
||||
{% if u.song %}
|
||||
<div id="uid" class="d-none">{{u.id}}</div>
|
||||
{% endif %}
|
||||
|
||||
{% if v %}
|
||||
<div id='tax' class="d-none">{% if v.patron or u.patron %}0{% else %}0.03{% endif %}</div>
|
||||
|
@ -36,5 +25,4 @@
|
|||
<div id="username" class="d-none">{{u.username}}</div>
|
||||
{% endif %}
|
||||
|
||||
<script src="{{ 'js/userpage.js' | asset }}"></script>
|
||||
{% endblock %}
|
||||
|
|
|
@ -14,6 +14,8 @@
|
|||
<div>
|
||||
<div class="comment">
|
||||
{% with comments=[c] %}
|
||||
{% set should_hide_username = true %}
|
||||
{% set should_hide_score = true %}
|
||||
{% include "comments.html" %}
|
||||
{% endwith %}
|
||||
</div>
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
|
||||
from . import fixture_accounts
|
||||
from . import util
|
||||
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
from files.helpers.const import RENDER_DEPTH_LIMIT
|
||||
from . import fixture_accounts
|
||||
from . import fixture_submissions
|
||||
from . import fixture_comments
|
||||
|
@ -158,10 +159,10 @@ def test_more_button_label_in_deep_threads(accounts, submissions, comments):
|
|||
# only look every 5 posts to make this test not _too_ unbearably slow
|
||||
view_post_response = alice_client.get(f'/post/{post.id}')
|
||||
assert 200 == view_post_response.status_code
|
||||
if i <= 8:
|
||||
assert f'More comments ({i - 8})' not in view_post_response.text
|
||||
if i <= RENDER_DEPTH_LIMIT - 1:
|
||||
assert f'More comments ({i - RENDER_DEPTH_LIMIT + 1})' not in view_post_response.text
|
||||
else:
|
||||
assert f'More comments ({i - 8})' in view_post_response.text
|
||||
assert f'More comments ({i - RENDER_DEPTH_LIMIT + 1})' in view_post_response.text
|
||||
|
||||
@util.no_rate_limit
|
||||
def test_bulk_update_descendant_count_quick(accounts, submissions, comments):
|
||||
|
|
74
files/tests/test_no_content.py
Normal file
74
files/tests/test_no_content.py
Normal file
|
@ -0,0 +1,74 @@
|
|||
from . import fixture_accounts
|
||||
from . import util
|
||||
|
||||
@util.no_rate_limit
|
||||
def test_no_content_submissions(accounts):
|
||||
client = accounts.client_for_account()
|
||||
|
||||
# get our formkey
|
||||
submit_get_response = client.get("/submit")
|
||||
assert submit_get_response.status_code == 200
|
||||
|
||||
title = '\u200e\u200e\u200e\u200e\u200e\u200e'
|
||||
body = util.generate_text()
|
||||
formkey = util.formkey_from(submit_get_response.text)
|
||||
|
||||
# test bad title against good content
|
||||
submit_post_response = client.post("/submit", data={
|
||||
"title": title,
|
||||
"body": body,
|
||||
"formkey": formkey,
|
||||
})
|
||||
|
||||
assert submit_post_response.status_code == 400
|
||||
|
||||
title, body = body, title
|
||||
# test good title against bad content
|
||||
submit_post_response = client.post("/submit", data={
|
||||
"title": title,
|
||||
"body": body,
|
||||
"formkey": formkey,
|
||||
})
|
||||
|
||||
assert submit_post_response.status_code == 400
|
||||
|
||||
@util.no_rate_limit
|
||||
def test_no_content_comments(accounts):
|
||||
client = accounts.client_for_account()
|
||||
|
||||
# get our formkey
|
||||
submit_get_response = client.get("/submit")
|
||||
assert submit_get_response.status_code == 200
|
||||
|
||||
# make the post
|
||||
post_title = util.generate_text()
|
||||
post_body = util.generate_text()
|
||||
submit_post_response = client.post("/submit", data={
|
||||
"title": post_title,
|
||||
"body": post_body,
|
||||
"formkey": util.formkey_from(submit_get_response.text),
|
||||
})
|
||||
|
||||
assert submit_post_response.status_code == 200
|
||||
assert post_title in submit_post_response.text
|
||||
assert post_body in submit_post_response.text
|
||||
|
||||
# verify it actually got posted
|
||||
root_response = client.get("/")
|
||||
assert root_response.status_code == 200
|
||||
assert post_title in root_response.text
|
||||
assert post_body in root_response.text
|
||||
|
||||
# yank the ID out
|
||||
post = util.ItemData.from_html(submit_post_response.text)
|
||||
|
||||
# post a comment child
|
||||
comment_body = '\ufeff\ufeff\ufeff\ufeff\ufeff'
|
||||
submit_comment_response = client.post("/comment", data={
|
||||
"parent_fullname": post.id_full,
|
||||
"parent_level": 1,
|
||||
"submission": post.id,
|
||||
"body": comment_body,
|
||||
"formkey": util.formkey_from(submit_post_response.text),
|
||||
})
|
||||
assert submit_comment_response.status_code == 400
|
4
files/tests/test_seed_db.py
Normal file
4
files/tests/test_seed_db.py
Normal file
|
@ -0,0 +1,4 @@
|
|||
from files.commands.seed_db import seed_db_worker
|
||||
|
||||
def test_seed_db():
|
||||
seed_db_worker()
|
|
@ -0,0 +1,28 @@
|
|||
"""remove users.song
|
||||
|
||||
Revision ID: ba8a214736eb
|
||||
Revises: 1f30a37b08a0
|
||||
Create Date: 2023-02-08 22:04:15.901498+00:00
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = 'ba8a214736eb'
|
||||
down_revision = '1f30a37b08a0'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_column('users', 'song')
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.add_column('users', sa.Column('song', sa.VARCHAR(length=50), autoincrement=False, nullable=True))
|
||||
# ### end Alembic commands ###
|
44
pg_hba.conf
44
pg_hba.conf
|
@ -1,44 +0,0 @@
|
|||
# Database and user names containing spaces, commas, quotes and other
|
||||
# special characters must be quoted. Quoting one of the keywords
|
||||
# "all", "sameuser", "samerole" or "replication" makes the name lose
|
||||
# its special character, and just match a database or username with
|
||||
# that name.
|
||||
#
|
||||
# This file is read on server startup and when the server receives a
|
||||
# SIGHUP signal. If you edit the file on a running system, you have to
|
||||
# SIGHUP the server for the changes to take effect, run "pg_ctl reload",
|
||||
# or execute "SELECT pg_reload_conf()".
|
||||
#
|
||||
# Put your actual configuration here
|
||||
# ----------------------------------
|
||||
#
|
||||
# If you want to allow non-local connections, you need to add more
|
||||
# "host" records. In that case you will also need to make PostgreSQL
|
||||
# listen on a non-local interface via the listen_addresses
|
||||
# configuration parameter, or via the -i or -h command line switches.
|
||||
|
||||
|
||||
|
||||
|
||||
# DO NOT DISABLE!
|
||||
# If you change this first entry you will need to make sure that the
|
||||
# database superuser can access the database using some other method.
|
||||
# Noninteractive access to all databases is required during automatic
|
||||
# maintenance (custom daily cronjobs, replication, and similar tasks).
|
||||
#
|
||||
# Database administrative login by Unix domain socket
|
||||
local all postgres trust
|
||||
|
||||
# TYPE DATABASE USER ADDRESS METHOD
|
||||
|
||||
# "local" is for Unix domain socket connections only
|
||||
local all all trust
|
||||
# IPv4 local connections:
|
||||
host all all 127.0.0.1/32 trust
|
||||
# IPv6 local connections:
|
||||
host all all ::1/128 trust
|
||||
# Allow replication connections from localhost, by a user with the
|
||||
# replication privilege.
|
||||
local replication all trust
|
||||
host replication all 127.0.0.1/32 trust
|
||||
host replication all ::1/128 trust
|
12
poetry.lock
generated
12
poetry.lock
generated
|
@ -1006,14 +1006,6 @@ category = "main"
|
|||
optional = false
|
||||
python-versions = "*"
|
||||
|
||||
[[package]]
|
||||
name = "youtube-dl"
|
||||
version = "2021.12.17"
|
||||
description = "YouTube video downloader"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
|
||||
[[package]]
|
||||
name = "zope.event"
|
||||
version = "4.5.0"
|
||||
|
@ -2228,10 +2220,6 @@ wrapt = [
|
|||
yattag = [
|
||||
{file = "yattag-1.14.0.tar.gz", hash = "sha256:5731a31cb7452c0c6930dd1a284e0170b39eee959851a2aceb8d6af4134a5fa8"},
|
||||
]
|
||||
youtube-dl = [
|
||||
{file = "youtube_dl-2021.12.17-py2.py3-none-any.whl", hash = "sha256:f1336d5de68647e0364a47b3c0712578e59ec76f02048ff5c50ef1c69d79cd55"},
|
||||
{file = "youtube_dl-2021.12.17.tar.gz", hash = "sha256:bc59e86c5d15d887ac590454511f08ce2c47698d5a82c27bfe27b5d814bbaed2"},
|
||||
]
|
||||
"zope.event" = [
|
||||
{file = "zope.event-4.5.0-py2.py3-none-any.whl", hash = "sha256:2666401939cdaa5f4e0c08cf7f20c9b21423b95e88f4675b1443973bdb080c42"},
|
||||
{file = "zope.event-4.5.0.tar.gz", hash = "sha256:5e76517f5b9b119acf37ca8819781db6c16ea433f7e2062c4afc2b6fbedb1330"},
|
||||
|
|
|
@ -6,7 +6,7 @@ authors = ["Your Name <you@example.com>"]
|
|||
license = "AGPL"
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
python = "^3.10" # updating to 3.11 causes instability; see https://github.com/themotte/rDrama/issues/446
|
||||
python = "~3.10" # updating to 3.11 causes instability; see https://github.com/themotte/rDrama/issues/446
|
||||
beautifulsoup4 = "*"
|
||||
bleach = "4.1.0"
|
||||
Flask = "*"
|
||||
|
@ -30,11 +30,10 @@ pyotp = "*"
|
|||
qrcode = "*"
|
||||
redis = "*"
|
||||
requests = "*"
|
||||
SQLAlchemy = "*"
|
||||
SQLAlchemy = "^1.4.43"
|
||||
user-agents = "*"
|
||||
psycopg2-binary = "*"
|
||||
pusher_push_notifications = "*"
|
||||
youtube-dl = "*"
|
||||
yattag = "*"
|
||||
webptools = "*"
|
||||
pytest = "*"
|
||||
|
|
18
readme.md
18
readme.md
|
@ -11,13 +11,17 @@ On Windows, Docker will pester you to pay them money for licensing. If you want
|
|||
|
||||
2 - Install [Git](https://git-scm.com/). If you're on Windows and want a GUI, [Github Desktop](https://desktop.github.com/) is quite nice.
|
||||
|
||||
3 - Run the following commands in the terminal:
|
||||
3 - Run the following commands in the terminal or command line for first-time setup:
|
||||
|
||||
```
|
||||
```sh
|
||||
git clone https://github.com/themotte/rDrama/
|
||||
|
||||
cd rDrama
|
||||
```
|
||||
|
||||
4 - Run the following command to start the site:
|
||||
|
||||
```sh
|
||||
docker-compose up --build
|
||||
```
|
||||
|
||||
|
@ -41,11 +45,11 @@ Database migrations are instructions for how to convert an out-of-date database
|
|||
|
||||
## Why use database migrations
|
||||
|
||||
Database migrations allow us to specify where data moves when there are schema changes. This is important when we're live -- if we rename the `comments.ban\_reason` column to `comments.reason\_banned` for naming consistency or whatever, and we do this by dropping the `ban\_reason` column and adding a `reason\_banned` column, we will lose all user data in that column. We don't want to do this. With migrations, we could instead specify that the operation in question should be a column rename, or, if the database engine does not support renaming columns, that we should do a three-step process of "add new column, migrate data over, drop old column".
|
||||
Database migrations allow us to specify where data moves when there are schema changes. This is important when we're live -- if we rename the `comments.ban_reason` column to `comments.reason_banned` for naming consistency or whatever, and we do this by dropping the `ban_reason` column and adding a `reason_banned` column, we will lose all user data in that column. We don't want to do this. With migrations, we could instead specify that the operation in question should be a column rename, or, if the database engine does not support renaming columns, that we should do a three-step process of "add new column, migrate data over, drop old column".
|
||||
|
||||
## Database schema change workflow
|
||||
|
||||
As an example, let's say we want to add a column `is\_flagged` to the `comments` table.
|
||||
As an example, let's say we want to add a column `is_flagged` to the `comments` table.
|
||||
|
||||
1. Update the `Comment` model in `files/classes/comment.py`
|
||||
```python
|
||||
|
@ -62,7 +66,7 @@ As an example, let's say we want to add a column `is\_flagged` to the `comments`
|
|||
./util/command.py db revision --autogenerate --message="add is_flagged field to comments"
|
||||
```
|
||||
|
||||
This will create a migration in the `migrations/versions` directory with a name like `migrations/versions/2022\_05\_23\_05\_38\_40\_9c27db0b3918\_add\_is\_flagged\_field\_to\_comments.py` and content like
|
||||
This will create a migration in the `migrations/versions` directory with a name like `migrations/versions/2022_05_23_05_38_40_9c27db0b3918_add_is_flagged_field_to_comments.py` and content like
|
||||
```python
|
||||
"""add is_flagged field to comments
|
||||
Revision ID: 9c27db0b3918
|
||||
|
@ -82,7 +86,7 @@ def downgrade():
|
|||
op.drop_column('comments', 'is_flagged')
|
||||
```
|
||||
|
||||
3. Examine the autogenerated migration to make sure that everything looks right (it adds the column you expected it to add and nothing else, all constraints are named, etc. If you see a `None` in one of the alembic operations, e.g. `op.create\_foreign\_key\_something(None, 'usernotes', 'users', ['author\_id'])`, please replace it with a descriptive string before you commit the migration).
|
||||
3. Examine the autogenerated migration to make sure that everything looks right (it adds the column you expected it to add and nothing else, all constraints are named, etc.) If you see a `None` in one of the alembic operations, e.g. `op.create_foreign_key_something(None, 'usernotes', 'users', ['author_id'])`, please replace it with a descriptive string before you commit the migration.
|
||||
|
||||
4. Restart the Docker container to make sure it works.
|
||||
|
||||
|
@ -90,6 +94,6 @@ def downgrade():
|
|||
docker-compose up --build
|
||||
```
|
||||
|
||||
## So what's up with schema.sql, can I just change that?
|
||||
## So what's up with original-schema.sql, can I just change that?
|
||||
|
||||
No, please do not do that. Instead, please make a migration as described above.
|
||||
|
|
1372
redis.conf
1372
redis.conf
File diff suppressed because it is too large
Load diff
|
@ -1,4 +1,5 @@
|
|||
|
||||
import functools
|
||||
import pprint
|
||||
import subprocess
|
||||
import sys
|
||||
|
@ -35,6 +36,11 @@ def _execute(command,**kwargs):
|
|||
|
||||
proc.wait()
|
||||
if check and proc.returncode != 0:
|
||||
print("STDOUT:")
|
||||
print(stdout)
|
||||
print("STDERR (not interlaced properly, sorry):")
|
||||
print(stderr)
|
||||
|
||||
raise subprocess.CalledProcessError(
|
||||
command,
|
||||
proc.returncode,
|
||||
|
@ -53,32 +59,10 @@ def _docker(command, **kwargs):
|
|||
return _execute([
|
||||
"docker-compose",
|
||||
"exec", '-T',
|
||||
"files",
|
||||
"site",
|
||||
] + command,
|
||||
**kwargs)
|
||||
|
||||
def _status_single(server):
|
||||
command = ['docker', 'container', 'inspect', '-f', '{{.State.Status}}', server]
|
||||
result = _execute(command, check=False).stdout.strip()
|
||||
return result
|
||||
|
||||
# this should really be yanked out of the docker-compose somehow
|
||||
_containers = ["themotte", "themotte_postgres", "themotte_redis"]
|
||||
|
||||
def _all_running():
|
||||
for container in _containers:
|
||||
if _status_single(container) != "running":
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def _any_exited():
|
||||
for container in _containers:
|
||||
if _status_single(container) == "exited":
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def _start():
|
||||
print("Starting containers in operation mode . . .")
|
||||
print(" If this takes a while, it's probably building the container.")
|
||||
|
@ -92,10 +76,26 @@ def _start():
|
|||
]
|
||||
result = _execute(command)
|
||||
|
||||
while not _all_running():
|
||||
if _any_exited():
|
||||
raise RuntimeError("Server exited prematurely")
|
||||
time.sleep(1)
|
||||
# alright this seems sketchy, bear with me
|
||||
|
||||
# previous versions of this code used the '--wait' command-line flag
|
||||
# the problem with --wait is that it waits for the container to be healthy and working
|
||||
# "but wait, isn't that what we want?"
|
||||
# ah, but see, if the container will *never* be healthy and working - say, if there's a flaw causing it to fail on startup - it just waits forever
|
||||
# so that's not actually useful
|
||||
|
||||
# previous versions of this code also had a check to see if the containers started up properly
|
||||
# but this is surprisingly annoying to do if we don't know the containers' names
|
||||
# docker-compose *can* do it, but you either have to use very new features that aren't supported on Ubuntu 22.04, or you have to go through a bunch of parsing pain
|
||||
# and it kind of doesn't seem necessary
|
||||
|
||||
# see, docker-compose in this form *will* wait until it's *attempted* to start each container.
|
||||
# so at this point in execution, either the containers are running, or they're crashed
|
||||
# if they're running, hey, problem solved, we're good
|
||||
# if they're crashed, y'know what, problem still solved! because our next command will fail
|
||||
|
||||
# maybe there's still a race condition? I dunno! Keep an eye on this.
|
||||
# If there is a race condition then you're stuck doing something gnarly with `docker-compose ps`. Good luck!
|
||||
|
||||
print(" Containers started!")
|
||||
|
||||
|
@ -106,7 +106,6 @@ def _stop():
|
|||
command = ['docker-compose','stop']
|
||||
print("Stopping containers . . .")
|
||||
result = _execute(command)
|
||||
time.sleep(1)
|
||||
return result
|
||||
|
||||
def _operation(name, commands):
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue