Merge branch 'frost' into feature-remove-holes

This commit is contained in:
justcool393 2023-02-24 18:46:52 -06:00
commit 4c2967cb93
76 changed files with 1186 additions and 3575 deletions

View file

@ -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" ]

View file

@ -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!

View file

@ -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

View file

@ -1,7 +1,6 @@
version: '2.3'
services:
files:
container_name: "themotte"
site:
build:
target: operation

View file

@ -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:

View file

@ -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"),

View file

@ -1,7 +0,0 @@
new BugController({
imageSprite: "/assets/images/fly-sprite.webp",
canDie: false,
minBugs: 10,
maxBugs: 20,
mouseOver: "multiply"
});

View file

@ -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)}}();

View file

@ -1,7 +0,0 @@
new BugController({
imageSprite: "/assets/images/fireflies.webp",
canDie: false,
minBugs: 10,
maxBugs: 30,
mouseOver: "multiply"
});

View file

@ -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});
}
}
}

View file

@ -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 *

View file

@ -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))

View file

@ -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

View file

@ -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):

View 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)

View file

@ -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:

View file

@ -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

View file

@ -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
View 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"])

View file

@ -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

View file

@ -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

View file

@ -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)

View file

@ -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.

View file

@ -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,
}

View file

@ -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:

View file

@ -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('&amp;','&')
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
View 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())

View file

@ -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)

View file

@ -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 *

View file

@ -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)

View file

@ -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![]({new_badge.path})\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![]({new_badge.path})\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![]({new_badge.path})\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![]({badge.path})\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![]({badge.path})\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)

View file

@ -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![]({image})"
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![]({url})"
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)

View file

@ -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)

View file

@ -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):

View file

@ -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")

View file

@ -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)

View file

@ -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![]({url})"
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![]({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, 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![]({image})"
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![]({url})"
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)

View file

@ -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![]({url})"
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

View file

@ -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()

View file

@ -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)

View file

@ -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]

View 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 %}

View file

@ -44,4 +44,6 @@
</div>
</div>
{% if FEATURES['AWARDS'] %}
<script src="{{ 'js/award_modal.js' | asset }}" data-cfasync="false"></script>
{% endif %}

View file

@ -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>

View file

@ -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 %}&nbsp;<bdi style="color: #{{c.author.titlecolor}}">&nbsp;{{c.author.customtitle | safe}}</bdi>{% endif %}
{% if c.author.customtitle and not should_hide_username -%}
&nbsp;<bdi style="color: #{{c.author.titlecolor}}">&nbsp;{{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 @@
&nbsp;
<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>

View 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 -%}

View file

@ -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 %}

View file

@ -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 %}

View file

@ -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 %}

View file

@ -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 %} &bull;{% 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 %}

View file

@ -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 %}

View file

@ -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>

View file

@ -384,7 +384,7 @@
&nbsp;
<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>

View file

@ -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 @@
&nbsp;
<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 %}

View file

@ -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>

View file

@ -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}}"

View file

@ -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>

View 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 %}

View file

@ -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 %}

View file

@ -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 %}

View file

@ -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 %}

View file

@ -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>

View file

@ -1,4 +1,3 @@
from . import fixture_accounts
from . import util

View file

@ -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):

View 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

View file

@ -0,0 +1,4 @@
from files.commands.seed_db import seed_db_worker
def test_seed_db():
seed_db_worker()

View file

@ -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 ###

View file

@ -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
View file

@ -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"},

View file

@ -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 = "*"

View file

@ -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

File diff suppressed because it is too large Load diff

View file

@ -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):